Replace createNextDescribe with nextTestSetup (#64817)

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

I took some time and [wrote a
codemod](https://gist.github.com/wyattjoh/0d4464427506cb02062a4729ca906b62)
that replaces the old usage of the `createNextDescribe` with the new
`nextTestSetup`. You'll likely have to turn on hiding of whitespace in
order to review, but this should primarily introduce no changes to the
test structure other than using the new mechanism now.

Closes NEXT-3178
This commit is contained in:
Wyatt Johnson 2024-04-25 12:06:12 -06:00 committed by GitHub
parent a6a6117197
commit c6320ed87a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
230 changed files with 27760 additions and 28255 deletions

View file

@ -1,6 +1,6 @@
import path from 'path'
import { outdent } from 'outdent'
import { FileRef, createNextDescribe } from 'e2e-utils'
import { FileRef, nextTestSetup } from 'e2e-utils'
import {
check,
getRedboxDescription,
@ -10,41 +10,40 @@ import {
retry,
} from 'next-test-utils'
createNextDescribe(
'Error overlay - RSC runtime errors',
{
describe('Error overlay - RSC runtime errors', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'rsc-runtime-errors')),
},
({ next }) => {
it('should show runtime errors if invalid client API from node_modules is executed', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
})
it('should show runtime errors if invalid client API from node_modules is executed', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
import { callClientApi } from 'client-package'
export default function Page() {
callClientApi()
return 'page'
}
`
)
)
const browser = await next.browser('/server')
const browser = await next.browser('/server')
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
const errorDescription = await getRedboxDescription(browser)
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
const errorDescription = await getRedboxDescription(browser)
expect(errorDescription).toContain(
`Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component`
)
})
expect(errorDescription).toContain(
`Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component`
)
})
it('should show runtime errors if invalid server API from node_modules is executed', async () => {
await next.patchFile(
'app/client/page.js',
outdent`
it('should show runtime errors if invalid server API from node_modules is executed', async () => {
await next.patchFile(
'app/client/page.js',
outdent`
'use client'
import { callServerApi } from 'server-package'
export default function Page() {
@ -52,101 +51,100 @@ createNextDescribe(
return 'page'
}
`
)
)
const browser = await next.browser('/client')
const browser = await next.browser('/client')
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
const errorDescription = await getRedboxDescription(browser)
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
const errorDescription = await getRedboxDescription(browser)
expect(errorDescription).toContain(
'Error: `cookies` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context'
)
})
expect(errorDescription).toContain(
'Error: `cookies` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context'
)
})
it('should show source code for jsx errors from server component', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
it('should show source code for jsx errors from server component', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
export default function Page() {
return <div>{alert('warn')}</div>
}
`
)
)
const browser = await next.browser('/server')
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
const browser = await next.browser('/server')
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
const errorDescription = await getRedboxDescription(browser)
const errorDescription = await getRedboxDescription(browser)
expect(errorDescription).toContain(`Error: alert is not defined`)
})
expect(errorDescription).toContain(`Error: alert is not defined`)
})
it('should show the userland code error trace when fetch failed error occurred', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
it('should show the userland code error trace when fetch failed error occurred', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
export default async function Page() {
await fetch('http://locahost:3000/xxxx')
return 'page'
}
`
)
const browser = await next.browser('/server')
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
)
const browser = await next.browser('/server')
await check(
async () => ((await hasRedbox(browser)) ? 'success' : 'fail'),
/success/
)
const source = await getRedboxSource(browser)
// Can show the original source code
expect(source).toContain('app/server/page.js')
expect(source).toContain(`await fetch('http://locahost:3000/xxxx')`)
})
const source = await getRedboxSource(browser)
// Can show the original source code
expect(source).toContain('app/server/page.js')
expect(source).toContain(`await fetch('http://locahost:3000/xxxx')`)
})
it('should contain nextjs version check in error overlay', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
it('should contain nextjs version check in error overlay', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
export default function Page() {
throw new Error('test')
}
`
)
const browser = await next.browser('/server')
)
const browser = await next.browser('/server')
await retry(async () => {
expect(await hasRedbox(browser)).toBe(true)
})
const versionText = await getVersionCheckerText(browser)
await expect(versionText).toMatch(/Next.js \([\w.-]+\)/)
await retry(async () => {
expect(await hasRedbox(browser)).toBe(true)
})
const versionText = await getVersionCheckerText(browser)
await expect(versionText).toMatch(/Next.js \([\w.-]+\)/)
})
it('should not show the bundle layer info in the file trace', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
it('should not show the bundle layer info in the file trace', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
export default function Page() {
throw new Error('test')
}
`
)
const browser = await next.browser('/server')
)
const browser = await next.browser('/server')
await retry(async () => {
expect(await hasRedbox(browser)).toBe(true)
})
const source = await getRedboxSource(browser)
expect(source).toContain('app/server/page.js')
expect(source).not.toContain('//app/server/page.js')
// Does not contain webpack traces in file path
expect(source).not.toMatch(/webpack(-internal:)?\/\//)
await retry(async () => {
expect(await hasRedbox(browser)).toBe(true)
})
}
)
const source = await getRedboxSource(browser)
expect(source).toContain('app/server/page.js')
expect(source).not.toContain('//app/server/page.js')
// Does not contain webpack traces in file path
expect(source).not.toMatch(/webpack(-internal:)?\/\//)
})
})

View file

@ -1,21 +1,20 @@
/* eslint-env jest */
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { getRedboxComponentStack, hasRedbox } from 'next-test-utils'
import path from 'path'
createNextDescribe(
'Component Stack in error overlay',
{
describe('Component Stack in error overlay', () => {
const { next } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'component-stack'),
},
({ next }) => {
it('should show a component stack on hydration error', async () => {
const browser = await next.browser('/')
})
expect(await hasRedbox(browser)).toBe(true)
it('should show a component stack on hydration error', async () => {
const browser = await next.browser('/')
if (process.env.TURBOPACK) {
expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(`
expect(await hasRedbox(browser)).toBe(true)
if (process.env.TURBOPACK) {
expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(`
"...
<App>
<Mismatch>
@ -26,8 +25,8 @@ createNextDescribe(
"server"
"client""
`)
} else {
expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(`
} else {
expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(`
"<Mismatch>
<main>
<Component>
@ -36,7 +35,6 @@ createNextDescribe(
"server"
"client""
`)
}
})
}
)
}
})
})

View file

@ -1,40 +1,38 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'app-dir - app routes errors',
{
describe('app-dir - app routes errors', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
describe('bad lowercase exports', () => {
it.each([
['get'],
['head'],
['options'],
['post'],
['put'],
['delete'],
['patch'],
])(
'should print an error when using lowercase %p in dev',
async (method: string) => {
await next.fetch('/lowercase/' + method)
})
await check(() => {
expect(next.cliOutput).toContain(
`Detected lowercase method '${method}' in`
)
expect(next.cliOutput).toContain(
`Export the uppercase '${method.toUpperCase()}' method name to fix this error.`
)
expect(next.cliOutput).toMatch(
/Detected lowercase method '.+' in '.+\/route\.js'\. Export the uppercase '.+' method name to fix this error\./
)
return 'yes'
}, 'yes')
}
)
})
}
)
describe('bad lowercase exports', () => {
it.each([
['get'],
['head'],
['options'],
['post'],
['put'],
['delete'],
['patch'],
])(
'should print an error when using lowercase %p in dev',
async (method: string) => {
await next.fetch('/lowercase/' + method)
await check(() => {
expect(next.cliOutput).toContain(
`Detected lowercase method '${method}' in`
)
expect(next.cliOutput).toContain(
`Export the uppercase '${method.toUpperCase()}' method name to fix this error.`
)
expect(next.cliOutput).toMatch(
/Detected lowercase method '.+' in '.+\/route\.js'\. Export the uppercase '.+' method name to fix this error\./
)
return 'yes'
}, 'yes')
}
)
})
})

View file

@ -1,4 +1,4 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import {
getRedboxCallStack,
hasRedbox,
@ -7,9 +7,8 @@ import {
getRedboxSource,
} from 'next-test-utils'
createNextDescribe(
'app dir - dynamic error trace',
{
describe('app dir - dynamic error trace', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
dependencies: {
swr: 'latest',
@ -25,18 +24,18 @@ createNextDescribe(
startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start',
buildCommand: 'pnpm build',
skipDeployment: true,
},
({ next }) => {
it('should show the error trace', async () => {
const browser = await next.browser('/')
await hasRedbox(browser)
await expandCallStack(browser)
const callStack = await getRedboxCallStack(browser)
})
if (skipped) return
expect(callStack).toContain('node_modules/headers-lib/index.mjs')
it('should show the error trace', async () => {
const browser = await next.browser('/')
await hasRedbox(browser)
await expandCallStack(browser)
const callStack = await getRedboxCallStack(browser)
const source = await getRedboxSource(browser)
expect(source).toContain('app/lib.js')
})
}
)
expect(callStack).toContain('node_modules/headers-lib/index.mjs')
const source = await getRedboxSource(browser)
expect(source).toContain('app/lib.js')
})
})

View file

@ -1,40 +1,38 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { hasRedbox } from 'next-test-utils'
createNextDescribe(
'develop - app-dir - edge errros hmr',
{
describe('develop - app-dir - edge errros hmr', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should recover from build errors when server component error', async () => {
const browser = await next.browser('/')
const clientComponentSource = await next.readFile('app/comp.server.js')
})
await next.patchFile('app/comp.server.js', (content) => {
return content.replace('{/* < */}', '<') // uncomment
})
it('should recover from build errors when server component error', async () => {
const browser = await next.browser('/')
const clientComponentSource = await next.readFile('app/comp.server.js')
expect(await hasRedbox(browser)).toBe(true)
await next.patchFile('app/comp.server.js', clientComponentSource)
expect(await hasRedbox(browser)).toBe(false)
await next.patchFile('app/comp.server.js', (content) => {
return content.replace('{/* < */}', '<') // uncomment
})
it('should recover from build errors when client component error', async () => {
const browser = await next.browser('/')
const clientComponentSource = await next.readFile('app/comp.client.js')
expect(await hasRedbox(browser)).toBe(true)
await next.patchFile('app/comp.client.js', (content) => {
return content.replace('{/* < */}', '<') // uncomment
})
await next.patchFile('app/comp.server.js', clientComponentSource)
expect(await hasRedbox(browser)).toBe(true)
expect(await hasRedbox(browser)).toBe(false)
})
await next.patchFile('app/comp.client.js', clientComponentSource)
it('should recover from build errors when client component error', async () => {
const browser = await next.browser('/')
const clientComponentSource = await next.readFile('app/comp.client.js')
expect(await hasRedbox(browser)).toBe(false)
await next.patchFile('app/comp.client.js', (content) => {
return content.replace('{/* < */}', '<') // uncomment
})
}
)
expect(await hasRedbox(browser)).toBe(true)
await next.patchFile('app/comp.client.js', clientComponentSource)
expect(await hasRedbox(browser)).toBe(false)
})
})

View file

@ -1,28 +1,26 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { hasRedbox } from 'next-test-utils'
createNextDescribe(
'HMR Move File',
{
describe('HMR Move File', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should work when moving a component to another directory', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('#hello-world-button').text()).toBe(
'hello world'
)
})
await next.renameFile('app/button.tsx', 'app/button2.tsx')
await next.patchFile('app/page.tsx', (content) =>
content.replace('./button', './button2')
)
it('should work when moving a component to another directory', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('#hello-world-button').text()).toBe(
'hello world'
)
expect(await hasRedbox(browser)).toBe(false)
await next.renameFile('app/button.tsx', 'app/button2.tsx')
await next.patchFile('app/page.tsx', (content) =>
content.replace('./button', './button2')
)
expect(await browser.elementByCss('#hello-world-button').text()).toBe(
'hello world'
)
})
}
)
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#hello-world-button').text()).toBe(
'hello world'
)
})
})

View file

@ -1,20 +1,18 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'multiple-compiles-single-route',
{
describe('multiple-compiles-single-route', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
it('should not compile additional matching paths', async () => {
const logs: string[] = []
next.on('stdout', (log) => {
logs.push(log)
})
await next.render('/about')
// Check if `/[slug]` is mentioned in the logs as being compiled
expect(logs.some((log) => log.includes('/[slug]'))).toBe(false)
})
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
it('should not compile additional matching paths', async () => {
const logs: string[] = []
next.on('stdout', (log) => {
logs.push(log)
})
}
)
await next.render('/about')
// Check if `/[slug]` is mentioned in the logs as being compiled
expect(logs.some((log) => log.includes('/[slug]'))).toBe(false)
})
})

View file

@ -1,27 +1,24 @@
import { type BrowserInterface } from 'next-webdriver'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'Strict Mode enabled by default',
{
describe('Strict Mode enabled by default', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
// experimental react is having issues with this use effect
// @acdlite will take a look
// TODO: remove this after react fixes the issue in experimental build.
if (process.env.__NEXT_EXPERIMENTAL_PPR) {
it('skip test for PPR', () => {})
return
}
// Recommended for tests that need a full browser
it('should work using browser', async () => {
const browser: BrowserInterface = await next.browser('/')
await check(async () => {
const text = await browser.elementByCss('p').text()
return text === '2' ? 'success' : `failed: ${text}`
}, 'success')
})
})
// experimental react is having issues with this use effect
// @acdlite will take a look
// TODO: remove this after react fixes the issue in experimental build.
if (process.env.__NEXT_EXPERIMENTAL_PPR) {
it('skip test for PPR', () => {})
return
}
)
// Recommended for tests that need a full browser
it('should work using browser', async () => {
const browser: BrowserInterface = await next.browser('/')
await check(async () => {
const text = await browser.elementByCss('p').text()
return text === '2' ? 'success' : `failed: ${text}`
}, 'success')
})
})

View file

@ -1,96 +1,94 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'
const envFile = '.env.development.local'
createNextDescribe(
`app-dir-hmr`,
{
describe(`app-dir-hmr`, () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
describe('filesystem changes', () => {
it('should not continously poll when hitting a not found page', async () => {
let requestCount = 0
})
const browser = await next.browser('/does-not-exist', {
beforePageLoad(page) {
page.on('request', (request) => {
const url = new URL(request.url())
if (url.pathname === '/does-not-exist') {
requestCount++
}
})
},
})
const body = await browser.elementByCss('body').text()
expect(body).toContain('404')
describe('filesystem changes', () => {
it('should not continously poll when hitting a not found page', async () => {
let requestCount = 0
await waitFor(3000)
expect(requestCount).toBe(1)
const browser = await next.browser('/does-not-exist', {
beforePageLoad(page) {
page.on('request', (request) => {
const url = new URL(request.url())
if (url.pathname === '/does-not-exist') {
requestCount++
}
})
},
})
const body = await browser.elementByCss('body').text()
expect(body).toContain('404')
it('should not break when renaming a folder', async () => {
const browser = await next.browser('/folder')
const text = await browser.elementByCss('h1').text()
expect(text).toBe('Hello')
await waitFor(3000)
// Rename folder
await next.renameFolder('app/folder', 'app/folder-renamed')
try {
// Should be 404 in a few seconds
await check(async () => {
const body = await browser.elementByCss('body').text()
expect(body).toContain('404')
return 'success'
}, 'success')
// The new page should be rendered
const newHTML = await next.render('/folder-renamed')
expect(newHTML).toContain('Hello')
} finally {
// Rename it back
await next.renameFolder('app/folder-renamed', 'app/folder')
}
})
it('should update server components pages when env files is changed (nodejs)', async () => {
const envContent = await next.readFile(envFile)
const browser = await next.browser('/env/node')
expect(await browser.elementByCss('p').text()).toBe('mac')
await next.patchFile(envFile, 'MY_DEVICE="ipad"')
try {
await check(async () => {
expect(await browser.elementByCss('p').text()).toBe('ipad')
return 'success'
}, /success/)
} finally {
await next.patchFile(envFile, envContent)
}
})
it('should update server components pages when env files is changed (edge)', async () => {
const envContent = await next.readFile(envFile)
const browser = await next.browser('/env/edge')
expect(await browser.elementByCss('p').text()).toBe('mac')
await next.patchFile(envFile, 'MY_DEVICE="ipad"')
try {
await check(async () => {
expect(await browser.elementByCss('p').text()).toBe('ipad')
return 'success'
}, /success/)
} finally {
await next.patchFile(envFile, envContent)
}
})
it('should have no unexpected action error for hmr', async () => {
expect(next.cliOutput).not.toContain('Unexpected action')
})
expect(requestCount).toBe(1)
})
}
)
it('should not break when renaming a folder', async () => {
const browser = await next.browser('/folder')
const text = await browser.elementByCss('h1').text()
expect(text).toBe('Hello')
// Rename folder
await next.renameFolder('app/folder', 'app/folder-renamed')
try {
// Should be 404 in a few seconds
await check(async () => {
const body = await browser.elementByCss('body').text()
expect(body).toContain('404')
return 'success'
}, 'success')
// The new page should be rendered
const newHTML = await next.render('/folder-renamed')
expect(newHTML).toContain('Hello')
} finally {
// Rename it back
await next.renameFolder('app/folder-renamed', 'app/folder')
}
})
it('should update server components pages when env files is changed (nodejs)', async () => {
const envContent = await next.readFile(envFile)
const browser = await next.browser('/env/node')
expect(await browser.elementByCss('p').text()).toBe('mac')
await next.patchFile(envFile, 'MY_DEVICE="ipad"')
try {
await check(async () => {
expect(await browser.elementByCss('p').text()).toBe('ipad')
return 'success'
}, /success/)
} finally {
await next.patchFile(envFile, envContent)
}
})
it('should update server components pages when env files is changed (edge)', async () => {
const envContent = await next.readFile(envFile)
const browser = await next.browser('/env/edge')
expect(await browser.elementByCss('p').text()).toBe('mac')
await next.patchFile(envFile, 'MY_DEVICE="ipad"')
try {
await check(async () => {
expect(await browser.elementByCss('p').text()).toBe('ipad')
return 'success'
}, /success/)
} finally {
await next.patchFile(envFile, envContent)
}
})
it('should have no unexpected action error for hmr', async () => {
expect(next.cliOutput).not.toContain('Unexpected action')
})
})
})

View file

@ -1,42 +1,39 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'app-render-error-log',
{
describe('app-render-error-log', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should log the correct values on app-render error', async () => {
const outputIndex = next.cliOutput.length
await next.fetch('/')
})
it('should log the correct values on app-render error', async () => {
const outputIndex = next.cliOutput.length
await next.fetch('/')
await check(() => next.cliOutput.slice(outputIndex), /at Page/)
const cliOutput = next.cliOutput.slice(outputIndex)
await check(() => next.cliOutput.slice(outputIndex), /at Page/)
const cliOutput = next.cliOutput.slice(outputIndex)
await check(() => cliOutput, /digest:/)
expect(cliOutput).toInclude('Error: boom')
expect(cliOutput).toInclude('at fn2 (./app/fn.ts')
expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/)
expect(cliOutput).toInclude('at Page (./app/page.tsx')
await check(() => cliOutput, /digest:/)
expect(cliOutput).toInclude('Error: boom')
expect(cliOutput).toInclude('at fn2 (./app/fn.ts')
expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/)
expect(cliOutput).toInclude('at Page (./app/page.tsx')
expect(cliOutput).not.toInclude('webpack-internal')
})
expect(cliOutput).not.toInclude('webpack-internal')
})
it('should log the correct values on app-render error with edge runtime', async () => {
const outputIndex = next.cliOutput.length
await next.fetch('/edge')
it('should log the correct values on app-render error with edge runtime', async () => {
const outputIndex = next.cliOutput.length
await next.fetch('/edge')
await check(() => next.cliOutput.slice(outputIndex), /at EdgePage/)
const cliOutput = next.cliOutput.slice(outputIndex)
await check(() => next.cliOutput.slice(outputIndex), /at EdgePage/)
const cliOutput = next.cliOutput.slice(outputIndex)
await check(() => cliOutput, /digest:/)
expect(cliOutput).toInclude('Error: boom')
expect(cliOutput).toInclude('at fn2 (./app/fn.ts')
expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/)
expect(cliOutput).toInclude('at EdgePage (./app/edge/page.tsx')
await check(() => cliOutput, /digest:/)
expect(cliOutput).toInclude('Error: boom')
expect(cliOutput).toInclude('at fn2 (./app/fn.ts')
expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/)
expect(cliOutput).toInclude('at EdgePage (./app/edge/page.tsx')
expect(cliOutput).not.toInclude('webpack-internal')
})
}
)
expect(cliOutput).not.toInclude('webpack-internal')
})
})

View file

@ -1,28 +1,26 @@
import { join } from 'path'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'asset-prefix',
{
describe('asset-prefix', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
},
({ next }) => {
it('should load the app properly without reloading', async () => {
const browser = await next.browser('/')
await browser.eval(`window.__v = 1`)
})
expect(await browser.elementByCss('div').text()).toBe('Hello World')
it('should load the app properly without reloading', async () => {
const browser = await next.browser('/')
await browser.eval(`window.__v = 1`)
await check(async () => {
const logs = await browser.log()
const hasError = logs.some((log) =>
log.message.includes('Failed to fetch')
)
return hasError ? 'error' : 'success'
}, 'success')
expect(await browser.elementByCss('div').text()).toBe('Hello World')
expect(await browser.eval(`window.__v`)).toBe(1)
})
}
)
await check(async () => {
const logs = await browser.log()
const hasError = logs.some((log) =>
log.message.includes('Failed to fetch')
)
return hasError ? 'error' : 'success'
}, 'success')
expect(await browser.eval(`window.__v`)).toBe(1)
})
})

View file

@ -1,13 +1,13 @@
import { join } from 'path'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { hasRedbox } from 'next-test-utils'
// Skipped in Turbopack, will be added later.
;(process.env.TURBOPACK ? describe.skip : describe)(
'Skipped in Turbopack',
() => {
createNextDescribe(
'optimizePackageImports - mui',
{
describe('optimizePackageImports - mui', () => {
const { next } = nextTestSetup({
env: {
NEXT_TEST_MODE: '1',
},
@ -18,31 +18,30 @@ import { hasRedbox } from 'next-test-utils'
'@emotion/react': '11.11.1',
'@emotion/styled': '11.11.0',
},
},
({ next }) => {
it('should support MUI', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
})
// Ensure that MUI is working
const $ = await next.render$('/mui')
expect(await $('#button').text()).toContain('button')
expect(await $('#typography').text()).toContain('typography')
const browser = await next.browser('/mui')
expect(await hasRedbox(browser)).toBe(false)
const modules = [...logs.matchAll(/\((\d+) modules\)/g)]
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, moduleCount] of modules) {
// Ensure that the number of modules is less than 1500 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1500)
}
it('should support MUI', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
}
)
// Ensure that MUI is working
const $ = await next.render$('/mui')
expect(await $('#button').text()).toContain('button')
expect(await $('#typography').text()).toContain('typography')
const browser = await next.browser('/mui')
expect(await hasRedbox(browser)).toBe(false)
const modules = [...logs.matchAll(/\((\d+) modules\)/g)]
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, moduleCount] of modules) {
// Ensure that the number of modules is less than 1500 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1500)
}
})
})
}
)

View file

@ -1,10 +1,9 @@
import { join } from 'path'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { hasRedbox } from 'next-test-utils'
createNextDescribe(
'optimizePackageImports - mui',
{
describe('optimizePackageImports - mui', () => {
const { next } = nextTestSetup({
env: {
NEXT_TEST_MODE: '1',
},
@ -16,12 +15,11 @@ createNextDescribe(
'@remixicon/react': '^4.2.0',
'@tremor/react': '^3.14.1',
},
},
({ next }) => {
it('should work', async () => {
// Without barrel optimization, the reproduction breaks.
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
})
}
)
})
it('should work', async () => {
// Without barrel optimization, the reproduction breaks.
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
})
})

View file

@ -1,12 +1,11 @@
import { join } from 'path'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
// Skipped in Turbopack, will be added later.
;(process.env.TURBOPACK ? describe.skip : describe)(
'Skipped in Turbopack',
() => {
createNextDescribe(
'optimizePackageImports - basic',
{
describe('optimizePackageImports - basic', () => {
const { next } = nextTestSetup({
env: {
NEXT_TEST_MODE: '1',
},
@ -18,114 +17,113 @@ import { createNextDescribe } from 'e2e-utils'
'@visx/visx': '3.3.0',
'recursive-barrel': '1.0.0',
},
},
({ next }) => {
it('app - should render the icons correctly without creating all the modules', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
})
const html = await next.render('/')
// Ensure the icons are rendered
expect(html).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
const modules = [
...logs.matchAll(
/Compiled (\/[\w-]*)*\s*in \d+(\.\d+)?(s|ms) \((\d+) modules\)/g
),
]
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, , , , moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
it('app - should render the icons correctly without creating all the modules', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
it('pages - should render the icons correctly without creating all the modules', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
const html = await next.render('/')
const html = await next.render('/pages-route')
// Ensure the icons are rendered
expect(html).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
// Ensure the icons are rendered
expect(html).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
const modules = [
...logs.matchAll(
/Compiled (\/[\w-]*)*\s*in \d+(\.\d+)?(s|ms) \((\d+) modules\)/g
),
]
const modules = [
...logs.matchAll(
/Compiled (\/[\w-]+)*\s*in \d+(\.\d+)?(s|ms) \((\d+) modules\)/g
),
]
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, , , , moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
})
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, , , , moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
it('pages - should render the icons correctly without creating all the modules', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
it('app - should optimize recursive wildcard export mapping', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
const html = await next.render('/pages-route')
await next.render('/recursive-barrel-app')
// Ensure the icons are rendered
expect(html).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
const modules = [...logs.matchAll(/\((\d+) modules\)/g)]
const modules = [
...logs.matchAll(
/Compiled (\/[\w-]+)*\s*in \d+(\.\d+)?(s|ms) \((\d+) modules\)/g
),
]
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, , , , moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
})
it('app - should optimize recursive wildcard export mapping', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
it('pages - should optimize recursive wildcard export mapping', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
await next.render('/recursive-barrel-app')
await next.render('/recursive-barrel')
const modules = [...logs.matchAll(/\((\d+) modules\)/g)]
const modules = [...logs.matchAll(/\((\d+) modules\)/g)]
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
})
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
it('pages - should optimize recursive wildcard export mapping', async () => {
let logs = ''
next.on('stdout', (log) => {
logs += log
})
it('should handle recursive wildcard exports', async () => {
const html = await next.render('/recursive')
expect(html).toContain('<h1>42</h1>')
})
await next.render('/recursive-barrel')
it('should support visx', async () => {
const html = await next.render('/visx')
expect(html).toContain('<linearGradient')
})
const modules = [...logs.matchAll(/\((\d+) modules\)/g)]
it('should not break "use client" directive in optimized packages', async () => {
const html = await next.render('/client')
expect(html).toContain('this is a client component')
})
expect(modules.length).toBeGreaterThanOrEqual(1)
for (const [, moduleCount] of modules) {
// Ensure that the number of modules is less than 1000 - otherwise we're
// importing the entire library.
expect(parseInt(moduleCount)).toBeLessThan(1000)
}
})
it('should support "use client" directive in barrel file', async () => {
const html = await next.render('/client-boundary')
expect(html).toContain('<button>0</button>')
})
}
)
it('should handle recursive wildcard exports', async () => {
const html = await next.render('/recursive')
expect(html).toContain('<h1>42</h1>')
})
it('should support visx', async () => {
const html = await next.render('/visx')
expect(html).toContain('<linearGradient')
})
it('should not break "use client" directive in optimized packages', async () => {
const html = await next.render('/client')
expect(html).toContain('this is a client component')
})
it('should support "use client" directive in barrel file', async () => {
const html = await next.render('/client-boundary')
expect(html).toContain('<button>0</button>')
})
})
}
)

View file

@ -1,11 +1,10 @@
import { join } from 'path'
import webdriver from 'next-webdriver'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'useDefineForClassFields SWC option',
{
describe('useDefineForClassFields SWC option', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
dependencies: {
mobx: '6.3.7',
@ -14,67 +13,66 @@ createNextDescribe(
'@types/node': 'latest',
'mobx-react': '7.2.1',
},
},
({ next }) => {
it('tsx should compile with useDefineForClassFields enabled', async () => {
let browser
try {
browser = await webdriver(next.url, '/')
await browser.elementByCss('#action').click()
await check(
() => browser.elementByCss('#name').text(),
/this is my name: next/
)
} finally {
if (browser) {
await browser.close()
}
})
it('tsx should compile with useDefineForClassFields enabled', async () => {
let browser
try {
browser = await webdriver(next.url, '/')
await browser.elementByCss('#action').click()
await check(
() => browser.elementByCss('#name').text(),
/this is my name: next/
)
} finally {
if (browser) {
await browser.close()
}
})
it("Initializes resident to undefined after the call to 'super()' when with useDefineForClassFields enabled", async () => {
let browser
try {
browser = await webdriver(next.url, '/animal')
expect(await browser.elementByCss('#dog').text()).toBe('')
expect(await browser.elementByCss('#dogDecl').text()).toBe('dog')
} finally {
if (browser) {
await browser.close()
}
}
})
async function matchLogs$(browser) {
let data_foundLog = false
let name_foundLog = false
const browserLogs = await browser.log('browser')
browserLogs.forEach((log) => {
if (log.message.includes('data changed')) {
data_foundLog = true
}
if (log.message.includes('name changed')) {
name_foundLog = true
}
})
return [data_foundLog, name_foundLog]
}
})
it('set accessors from base classes wont get triggered with useDefineForClassFields enabled', async () => {
let browser
try {
browser = await webdriver(next.url, '/derived')
await matchLogs$(browser).then(([data_foundLog, name_foundLog]) => {
expect(data_foundLog).toBe(true)
expect(name_foundLog).toBe(false)
})
} finally {
if (browser) {
await browser.close()
}
it("Initializes resident to undefined after the call to 'super()' when with useDefineForClassFields enabled", async () => {
let browser
try {
browser = await webdriver(next.url, '/animal')
expect(await browser.elementByCss('#dog').text()).toBe('')
expect(await browser.elementByCss('#dogDecl').text()).toBe('dog')
} finally {
if (browser) {
await browser.close()
}
}
})
async function matchLogs$(browser) {
let data_foundLog = false
let name_foundLog = false
const browserLogs = await browser.log('browser')
browserLogs.forEach((log) => {
if (log.message.includes('data changed')) {
data_foundLog = true
}
if (log.message.includes('name changed')) {
name_foundLog = true
}
})
return [data_foundLog, name_foundLog]
}
)
it('set accessors from base classes wont get triggered with useDefineForClassFields enabled', async () => {
let browser
try {
browser = await webdriver(next.url, '/derived')
await matchLogs$(browser).then(([data_foundLog, name_foundLog]) => {
expect(data_foundLog).toBe(true)
expect(name_foundLog).toBe(false)
})
} finally {
if (browser) {
await browser.close()
}
}
})
})

View file

@ -1,24 +1,22 @@
import { join } from 'path'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'emotion SWC option',
{
describe('emotion SWC option', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
dependencies: {
'@emotion/react': '11.10.4',
'@emotion/styled': '11.10.4',
'@emotion/cache': '11.10.3',
},
},
({ next }) => {
it('should have styling from the css prop', async () => {
const browser = await next.browser('/')
})
const color = await browser
.elementByCss('#test-element')
.getComputedCss('background-color')
expect(color).toBe('rgb(255, 192, 203)')
})
}
)
it('should have styling from the css prop', async () => {
const browser = await next.browser('/')
const color = await browser
.elementByCss('#test-element')
.getComputedCss('background-color')
expect(color).toBe('rgb(255, 192, 203)')
})
})

View file

@ -1,172 +1,170 @@
import { join } from 'path'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'node builtins',
{
describe('node builtins', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'node-builtins'),
},
({ next }) => {
it('should have polyfilled node.js builtins for the browser correctly', async () => {
const browser = await next.browser('/')
})
await browser.waitForCondition('window.didRender', 15000)
it('should have polyfilled node.js builtins for the browser correctly', async () => {
const browser = await next.browser('/')
const data = await browser
.waitForElementByCss('#node-browser-polyfills')
.text()
const parsedData = JSON.parse(data)
await browser.waitForCondition('window.didRender', 15000)
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toBe('browser')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = await browser
.waitForElementByCss('#node-browser-polyfills')
.text()
const parsedData = JSON.parse(data)
it('should have polyfilled node.js builtins for the browser correctly in client component', async () => {
const browser = await next.browser('/client-component')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toBe('browser')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
await browser.waitForCondition('window.didRender', 15000)
it('should have polyfilled node.js builtins for the browser correctly in client component', async () => {
const browser = await next.browser('/client-component')
const data = await browser
.waitForElementByCss('#node-browser-polyfills')
.text()
const parsedData = JSON.parse(data)
await browser.waitForCondition('window.didRender', 15000)
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toBe('browser')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = await browser
.waitForElementByCss('#node-browser-polyfills')
.text()
const parsedData = JSON.parse(data)
it('should support node.js builtins', async () => {
const $ = await next.render$('/server')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toBe('browser')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
it('should support node.js builtins', async () => {
const $ = await next.render$('/server')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-server')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
it('should support node.js builtins prefixed by node:', async () => {
const $ = await next.render$('/server-node-schema')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-server')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
it('should support node.js builtins prefixed by node:', async () => {
const $ = await next.render$('/server-node-schema')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-server')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
it('should support node.js builtins in server component', async () => {
const $ = await next.render$('/server-component')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-server')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
it('should support node.js builtins in server component', async () => {
const $ = await next.render$('/server-component')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
it('should support node.js builtins prefixed by node: in server component', async () => {
const $ = await next.render$('/server-component/node-schema')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
it('should support node.js builtins prefixed by node: in server component', async () => {
const $ = await next.render$('/server-component/node-schema')
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
}
)
const data = $('#node-browser-polyfills').text()
const parsedData = JSON.parse(data)
expect(parsedData.vm).toBe(105)
expect(parsedData.hash).toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
)
expect(parsedData.buffer).toBe('hello world')
expect(parsedData.stream).toBe(true)
expect(parsedData.assert).toBe(true)
expect(parsedData.constants).toBe(7)
expect(parsedData.domain).toBe(true)
expect(parsedData.http).toBe(true)
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
expect(parsedData.timers).toBe(true)
})
})

View file

@ -1,88 +1,85 @@
import webdriver from 'next-webdriver'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
createNextDescribe(
'styled-components SWC transform',
{
describe('styled-components SWC transform', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
'styled-components': '5.3.3',
},
},
({ next }) => {
async function matchLogs$(browser) {
let foundLog = false
})
async function matchLogs$(browser) {
let foundLog = false
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log('browser')
browserLogs.forEach((log) => {
if (log.message.includes('Warning: Prop `%s` did not match.')) {
foundLog = true
}
})
return foundLog
}
it('should not have hydration mismatch with styled-components transform enabled', async () => {
let browser
try {
browser = await webdriver(next.url, '/')
// Compile /_error
await fetchViaHTTP(next.url, '/404')
// Try 4 times to be sure there is no mismatch
expect(await matchLogs$(browser)).toBe(false)
await browser.refresh()
expect(await matchLogs$(browser)).toBe(false)
await browser.refresh()
expect(await matchLogs$(browser)).toBe(false)
await browser.refresh()
expect(await matchLogs$(browser)).toBe(false)
} finally {
if (browser) {
await browser.close()
}
browserLogs.forEach((log) => {
if (log.message.includes('Warning: Prop `%s` did not match.')) {
foundLog = true
}
})
it('should render the page with correct styles', async () => {
const browser = await webdriver(next.url, '/')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('#btn')).color`
)
).toBe('rgb(255, 255, 255)')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('#wrap-div')).color`
)
).toBe('rgb(0, 0, 0)')
})
it('should enable the display name transform by default', async () => {
// make sure the index chunk gets generated
const html = await next.render('/')
expect(html).toContain('pages__Button')
})
it('should contain styles in initial HTML', async () => {
const html = await renderViaHTTP(next.url, '/')
expect(html).toContain('background:transparent')
expect(html).toContain('color:white')
})
it('should only render once on the server per request', async () => {
const outputs = []
next.on('stdout', (args) => {
outputs.push(args)
})
await renderViaHTTP(next.url, '/')
expect(
outputs.filter((output) => output.trim() === '__render__').length
).toBe(1)
})
return foundLog
}
)
it('should not have hydration mismatch with styled-components transform enabled', async () => {
let browser
try {
browser = await webdriver(next.url, '/')
// Compile /_error
await fetchViaHTTP(next.url, '/404')
// Try 4 times to be sure there is no mismatch
expect(await matchLogs$(browser)).toBe(false)
await browser.refresh()
expect(await matchLogs$(browser)).toBe(false)
await browser.refresh()
expect(await matchLogs$(browser)).toBe(false)
await browser.refresh()
expect(await matchLogs$(browser)).toBe(false)
} finally {
if (browser) {
await browser.close()
}
}
})
it('should render the page with correct styles', async () => {
const browser = await webdriver(next.url, '/')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('#btn')).color`
)
).toBe('rgb(255, 255, 255)')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('#wrap-div')).color`
)
).toBe('rgb(0, 0, 0)')
})
it('should enable the display name transform by default', async () => {
// make sure the index chunk gets generated
const html = await next.render('/')
expect(html).toContain('pages__Button')
})
it('should contain styles in initial HTML', async () => {
const html = await renderViaHTTP(next.url, '/')
expect(html).toContain('background:transparent')
expect(html).toContain('color:white')
})
it('should only render once on the server per request', async () => {
const outputs = []
next.on('stdout', (args) => {
outputs.push(args)
})
await renderViaHTTP(next.url, '/')
expect(
outputs.filter((output) => output.trim() === '__render__').length
).toBe(1)
})
})

View file

@ -1,19 +1,17 @@
import { join } from 'path'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'theme-ui SWC option',
{
describe('theme-ui SWC option', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
dependencies: {
'theme-ui': '0.12.0',
},
},
({ next }) => {
it('should have theme provided styling', async () => {
const browser = await next.browser('/')
const color = await browser.elementByCss('#hello').getComputedCss('color')
expect(color).toBe('rgb(51, 51, 238)')
})
}
)
})
it('should have theme provided styling', async () => {
const browser = await next.browser('/')
const color = await browser.elementByCss('#hello').getComputedCss('color')
expect(color).toBe('rgb(51, 51, 238)')
})
})

View file

@ -1,14 +1,11 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'Handles Duplicate Pages',
{
describe('Handles Duplicate Pages', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('Shows warning in development', async () => {
await next.render('/hello')
expect(next.cliOutput).toMatch(/Duplicate page detected/)
})
}
)
})
it('Shows warning in development', async () => {
await next.render('/hello')
expect(next.cliOutput).toMatch(/Duplicate page detected/)
})
})

View file

@ -1,37 +1,36 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import https from 'https'
import { renderViaHTTP, shouldRunTurboDevTest } from 'next-test-utils'
createNextDescribe(
'experimental-https-server (generated certificate)',
{
describe('experimental-https-server (generated certificate)', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
startCommand: `pnpm next ${
shouldRunTurboDevTest() ? 'dev --turbo' : 'dev'
} --experimental-https`,
skipStart: !process.env.CI,
},
({ next }) => {
if (!process.env.CI) {
console.warn('only runs on CI as it requires administrator privileges')
it('only runs on CI as it requires administrator privileges', () => {})
return
}
})
if (skipped) return
const agent = new https.Agent({
rejectUnauthorized: false,
})
it('should successfully load the app in app dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/1', undefined, { agent })
expect(html).toContain('Hello from App')
})
it('should successfully load the app in pages dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/2', undefined, { agent })
expect(html).toContain('Hello from Pages')
})
if (!process.env.CI) {
console.warn('only runs on CI as it requires administrator privileges')
it('only runs on CI as it requires administrator privileges', () => {})
return
}
)
const agent = new https.Agent({
rejectUnauthorized: false,
})
it('should successfully load the app in app dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/1', undefined, { agent })
expect(html).toContain('Hello from App')
})
it('should successfully load the app in pages dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/2', undefined, { agent })
expect(html).toContain('Hello from Pages')
})
})

View file

@ -1,30 +1,27 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import https from 'https'
import { renderViaHTTP, shouldRunTurboDevTest } from 'next-test-utils'
createNextDescribe(
'experimental-https-server (provided certificate)',
{
describe('experimental-https-server (provided certificate)', () => {
const { next } = nextTestSetup({
files: __dirname,
startCommand: `pnpm next ${
shouldRunTurboDevTest() ? 'dev --turbo' : 'dev'
} --experimental-https --experimental-https-key ./certificates/localhost-key.pem --experimental-https-cert ./certificates/localhost.pem`,
},
({ next }) => {
const agent = new https.Agent({
rejectUnauthorized: false,
})
})
const agent = new https.Agent({
rejectUnauthorized: false,
})
it('should successfully load the app in app dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/1', undefined, { agent })
expect(html).toContain('Hello from App')
})
it('should successfully load the app in app dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/1', undefined, { agent })
expect(html).toContain('Hello from App')
})
it('should successfully load the app in pages dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/2', undefined, { agent })
expect(html).toContain('Hello from Pages')
})
}
)
it('should successfully load the app in pages dir', async () => {
expect(next.url).toInclude('https://')
const html = await renderViaHTTP(next.url, '/2', undefined, { agent })
expect(html).toContain('Hello from Pages')
})
})

View file

@ -1,66 +1,64 @@
import { check, getRedboxSource, hasRedbox } from 'next-test-utils'
import stripAnsi from 'strip-ansi'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'middleware - development errors',
{
describe('middleware - development errors', () => {
const { next } = nextTestSetup({
files: __dirname,
env: { __NEXT_TEST_WITH_DEVTOOL: '1' },
},
({ next }) => {
beforeEach(async () => {
await next.stop()
})
})
beforeEach(async () => {
await next.stop()
})
describe('when middleware throws synchronously', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
describe('when middleware throws synchronously', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
export default function () {
throw new Error('boom')
}`
)
)
await next.start()
})
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(() => {
if (process.env.TURBOPACK) {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ Object.__TURBOPACK__default__export__ \[as handler\]/
)
} else {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ Object.default \[as handler\]/
)
}
expect(stripAnsi(next.cliOutput)).toMatch(/boom/)
return 'success'
}, 'success')
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
})
await next.start()
})
describe('when middleware contains an unhandled rejection', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(() => {
if (process.env.TURBOPACK) {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ Object.__TURBOPACK__default__export__ \[as handler\]/
)
} else {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ Object.default \[as handler\]/
)
}
expect(stripAnsi(next.cliOutput)).toMatch(/boom/)
return 'success'
}, 'success')
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
})
})
describe('when middleware contains an unhandled rejection', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
import { NextResponse } from 'next/server'
async function throwError() {
throw new Error('async boom!')
@ -69,114 +67,114 @@ createNextDescribe(
throwError()
return NextResponse.next()
}`
)
)
await next.start()
})
it('logs the error correctly', async () => {
await next.fetch('/')
await check(
() => stripAnsi(next.cliOutput),
new RegExp(`unhandledRejection: Error: async boom!`, 'm')
)
// expect(output).not.toContain(
// 'webpack-internal:///(middleware)/./middleware.js'
// )
})
it('does not render the error', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
await next.start()
})
describe('when running invalid dynamic code with eval', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
it('logs the error correctly', async () => {
await next.fetch('/')
await check(
() => stripAnsi(next.cliOutput),
new RegExp(`unhandledRejection: Error: async boom!`, 'm')
)
// expect(output).not.toContain(
// 'webpack-internal:///(middleware)/./middleware.js'
// )
})
it('does not render the error', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
})
describe('when running invalid dynamic code with eval', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
import { NextResponse } from 'next/server'
export default function () {
eval('test')
return NextResponse.next()
}`
)
)
await next.start()
})
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(() => {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ eval/
)
expect(stripAnsi(next.cliOutput)).toMatch(/test is not defined/)
return 'success'
}, 'success')
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxSource(browser)).toContain(`eval('test')`)
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
})
await next.start()
})
describe('when throwing while loading the module', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(() => {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ eval/
)
expect(stripAnsi(next.cliOutput)).toMatch(/test is not defined/)
return 'success'
}, 'success')
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxSource(browser)).toContain(`eval('test')`)
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
})
})
describe('when throwing while loading the module', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
import { NextResponse } from 'next/server'
throw new Error('booooom!')
export default function () {
return NextResponse.next()
}`
)
await next.start()
})
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(() => {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ <unknown>/
)
expect(stripAnsi(next.cliOutput)).toMatch(/booooom!/)
return 'success'
}, 'success')
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
const source = await getRedboxSource(browser)
expect(source).toContain(`throw new Error('booooom!')`)
expect(source).toContain('middleware.js')
expect(source).not.toContain('//middleware.js')
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
})
)
await next.start()
})
describe('when there is an unhandled rejection while loading the module', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(() => {
expect(stripAnsi(next.cliOutput)).toMatch(
/middleware.js \(\d+:\d+\) @ <unknown>/
)
expect(stripAnsi(next.cliOutput)).toMatch(/booooom!/)
return 'success'
}, 'success')
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
const source = await getRedboxSource(browser)
expect(source).toContain(`throw new Error('booooom!')`)
expect(source).toContain('middleware.js')
expect(source).not.toContain('//middleware.js')
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
})
})
describe('when there is an unhandled rejection while loading the module', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
import { NextResponse } from 'next/server'
(async function(){
throw new Error('you shall see me')
@ -185,127 +183,124 @@ createNextDescribe(
export default function () {
return NextResponse.next()
}`
)
)
await next.start()
})
it('logs the error correctly', async () => {
await next.fetch('/')
await check(
() => stripAnsi(next.cliOutput),
new RegExp(`unhandledRejection: Error: you shall see me`, 'm')
)
// expect(output).not.toContain(
// 'webpack-internal:///(middleware)/./middleware.js'
// )
})
it('does not render the error', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
await next.start()
})
describe('when there is an unhandled rejection while loading a dependency', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
it('logs the error correctly', async () => {
await next.fetch('/')
await check(
() => stripAnsi(next.cliOutput),
new RegExp(`unhandledRejection: Error: you shall see me`, 'm')
)
// expect(output).not.toContain(
// 'webpack-internal:///(middleware)/./middleware.js'
// )
})
it('does not render the error', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
})
describe('when there is an unhandled rejection while loading a dependency', () => {
beforeEach(async () => {
await next.patchFile(
'middleware.js',
`
import { NextResponse } from 'next/server'
import './lib/unhandled'
export default function () {
return NextResponse.next()
}`
)
)
await next.start()
})
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(
() => stripAnsi(next.cliOutput),
new RegExp(
` uncaughtException: Error: This file asynchronously fails while loading`,
'm'
)
)
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
it('does not render the error', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
await next.start()
})
describe('when there is a compilation error from boot', () => {
beforeEach(async () => {
await next.patchFile('middleware.js', `export default function () }`)
it('logs the error correctly', async () => {
await next.fetch('/')
const output = stripAnsi(next.cliOutput)
await check(
() => stripAnsi(next.cliOutput),
new RegExp(
` uncaughtException: Error: This file asynchronously fails while loading`,
'm'
)
)
expect(output).not.toContain(
'webpack-internal:///(middleware)/./middleware.js'
)
})
await next.start()
})
it('does not render the error', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
})
it('logs the error correctly', async () => {
await next.fetch('/')
await check(async () => {
expect(next.cliOutput).toContain(`Expected '{', got '}'`)
expect(
next.cliOutput.split(`Expected '{', got '}'`).length
).toBeGreaterThanOrEqual(2)
describe('when there is a compilation error from boot', () => {
beforeEach(async () => {
await next.patchFile('middleware.js', `export default function () }`)
return 'success'
}, 'success')
})
await next.start()
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
it('logs the error correctly', async () => {
await next.fetch('/')
await check(async () => {
expect(next.cliOutput).toContain(`Expected '{', got '}'`)
expect(
await browser.elementByCss('#nextjs__container_errors_desc').text()
).toEqual('Failed to compile')
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
next.cliOutput.split(`Expected '{', got '}'`).length
).toBeGreaterThanOrEqual(2)
return 'success'
}, 'success')
})
describe('when there is a compilation error after boot', () => {
beforeEach(async () => {
await next.patchFile('middleware.js', `export default function () {}`)
await next.start()
})
it('logs the error correctly', async () => {
await next.patchFile('middleware.js', `export default function () }`)
await next.fetch('/')
await check(() => {
expect(next.cliOutput).toContain(`Expected '{', got '}'`)
expect(next.cliOutput.split(`Expected '{', got '}'`).length).toEqual(
2
)
return 'success'
}, 'success')
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
await next.patchFile('middleware.js', `export default function () }`)
expect(await hasRedbox(browser)).toBe(true)
await next.patchFile('middleware.js', `export default function () {}`)
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(true)
expect(
await browser.elementByCss('#nextjs__container_errors_desc').text()
).toEqual('Failed to compile')
await next.patchFile('middleware.js', `export default function () {}`)
await hasRedbox(browser)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
}
)
})
describe('when there is a compilation error after boot', () => {
beforeEach(async () => {
await next.patchFile('middleware.js', `export default function () {}`)
await next.start()
})
it('logs the error correctly', async () => {
await next.patchFile('middleware.js', `export default function () }`)
await next.fetch('/')
await check(() => {
expect(next.cliOutput).toContain(`Expected '{', got '}'`)
expect(next.cliOutput.split(`Expected '{', got '}'`).length).toEqual(2)
return 'success'
}, 'success')
})
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
expect(await hasRedbox(browser)).toBe(false)
await next.patchFile('middleware.js', `export default function () }`)
expect(await hasRedbox(browser)).toBe(true)
await next.patchFile('middleware.js', `export default function () {}`)
expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
})
})
})

View file

@ -1,10 +1,9 @@
/* eslint-env jest */
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'Deprecated @next/font warning',
{
describe('Deprecated @next/font warning', () => {
const { next, skipped } = nextTestSetup({
files: {
'pages/index.js': '',
},
@ -14,34 +13,34 @@ createNextDescribe(
'@next/font': 'canary',
},
skipStart: true,
},
({ next }) => {
it('should warn if @next/font is in deps', async () => {
await next.start()
await check(() => next.cliOutput, /ready/i)
await check(
() => next.cliOutput,
new RegExp('please use the built-in `next/font` instead')
)
})
if (skipped) return
await next.stop()
await next.clean()
})
it('should warn if @next/font is in deps', async () => {
await next.start()
await check(() => next.cliOutput, /ready/i)
await check(
() => next.cliOutput,
new RegExp('please use the built-in `next/font` instead')
)
it('should not warn if @next/font is not in deps', async () => {
// Remove @next/font from deps
const packageJson = JSON.parse(await next.readFile('package.json'))
delete packageJson.dependencies['@next/font']
await next.patchFile('package.json', JSON.stringify(packageJson))
await next.stop()
await next.clean()
})
await next.start()
await check(() => next.cliOutput, /ready/i)
expect(next.cliOutput).not.toInclude(
'please use the built-in `next/font` instead'
)
it('should not warn if @next/font is not in deps', async () => {
// Remove @next/font from deps
const packageJson = JSON.parse(await next.readFile('package.json'))
delete packageJson.dependencies['@next/font']
await next.patchFile('package.json', JSON.stringify(packageJson))
await next.stop()
await next.clean()
})
}
)
await next.start()
await check(() => next.cliOutput, /ready/i)
expect(next.cliOutput).not.toInclude(
'please use the built-in `next/font` instead'
)
await next.stop()
await next.clean()
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,46 +1,43 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'custom-app-hmr',
{
describe('custom-app-hmr', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should not do full reload when simply editing _app.js', async () => {
const customAppFilePath = 'pages/_app.js'
const browser = await next.browser('/')
await browser.eval('window.hmrConstantValue = "should-not-change"')
})
it('should not do full reload when simply editing _app.js', async () => {
const customAppFilePath = 'pages/_app.js'
const browser = await next.browser('/')
await browser.eval('window.hmrConstantValue = "should-not-change"')
const customAppContent = await next.readFile(customAppFilePath)
const newCustomAppContent = customAppContent.replace(
'hmr text origin',
'hmr text changed'
)
await next.patchFile(customAppFilePath, newCustomAppContent)
const customAppContent = await next.readFile(customAppFilePath)
const newCustomAppContent = customAppContent.replace(
'hmr text origin',
'hmr text changed'
)
await next.patchFile(customAppFilePath, newCustomAppContent)
await check(async () => {
const pText = await browser.elementByCss('h1').text()
expect(pText).toBe('hmr text changed')
await check(async () => {
const pText = await browser.elementByCss('h1').text()
expect(pText).toBe('hmr text changed')
// Should keep the value on window, which indicates there's no full reload
const hmrConstantValue = await browser.eval('window.hmrConstantValue')
expect(hmrConstantValue).toBe('should-not-change')
// Should keep the value on window, which indicates there's no full reload
const hmrConstantValue = await browser.eval('window.hmrConstantValue')
expect(hmrConstantValue).toBe('should-not-change')
return 'success'
}, 'success')
return 'success'
}, 'success')
await next.patchFile(customAppFilePath, customAppContent)
await check(async () => {
const pText = await browser.elementByCss('h1').text()
expect(pText).toBe('hmr text origin')
await next.patchFile(customAppFilePath, customAppContent)
await check(async () => {
const pText = await browser.elementByCss('h1').text()
expect(pText).toBe('hmr text origin')
// Should keep the value on window, which indicates there's no full reload
const hmrConstantValue = await browser.eval('window.hmrConstantValue')
expect(hmrConstantValue).toBe('should-not-change')
// Should keep the value on window, which indicates there's no full reload
const hmrConstantValue = await browser.eval('window.hmrConstantValue')
expect(hmrConstantValue).toBe('should-not-change')
return 'success'
}, 'success')
})
}
)
return 'success'
}, 'success')
})
})

View file

@ -1,48 +1,45 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import fs from 'fs-extra'
import { hasRedbox } from 'next-test-utils'
import path from 'path'
createNextDescribe(
'repeated-dev-edits',
{
describe('repeated-dev-edits', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
it('should not break the hydration ', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('version-1')
})
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
it('should not break the hydration ', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('version-1')
const pagePath = 'pages/index.tsx'
const pageContent = String(
await fs.readFile(path.join(__dirname, pagePath))
)
const pagePath = 'pages/index.tsx'
const pageContent = String(
await fs.readFile(path.join(__dirname, pagePath))
)
await next.patchFile(
pagePath,
pageContent.replaceAll('version-1', 'version-2')
)
await browser.waitForElementByCss('#version-2')
expect(await browser.elementByCss('p').text()).toBe('version-2')
await next.patchFile(
pagePath,
pageContent.replaceAll('version-1', 'version-2')
)
await browser.waitForElementByCss('#version-2')
expect(await browser.elementByCss('p').text()).toBe('version-2')
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()
await next.patchFile(
pagePath,
pageContent.replaceAll('version-1', 'version-3')
)
await browser.waitForElementByCss('#version-3')
expect(await browser.elementByCss('p').text()).toBe('version-3')
await next.patchFile(
pagePath,
pageContent.replaceAll('version-1', 'version-3')
)
await browser.waitForElementByCss('#version-3')
expect(await browser.elementByCss('p').text()).toBe('version-3')
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()
await browser.refresh()
await browser.refresh()
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()
})
}
)
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()
})
})

View file

@ -1,19 +1,18 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
import { join } from 'path'
createNextDescribe(
'watch-config-file',
{
files: join(__dirname, 'fixture'),
},
({ next }) => {
it('should output config file change', async () => {
await check(async () => next.cliOutput, /ready/i)
await check(async () => {
await next.patchFile(
'next.config.js',
`
describe('watch-config-file', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
})
it('should output config file change', async () => {
await check(async () => next.cliOutput, /ready/i)
await check(async () => {
await next.patchFile(
'next.config.js',
`
console.log(${Date.now()})
const nextConfig = {
reactStrictMode: true,
@ -28,11 +27,10 @@ createNextDescribe(
},
}
module.exports = nextConfig`
)
return next.cliOutput
}, /Found a change in next\.config\.js\. Restarting the server to apply the changes\.\.\./)
)
return next.cliOutput
}, /Found a change in next\.config\.js\. Restarting the server to apply the changes\.\.\./)
await check(() => next.fetch('/about').then((res) => res.status), 200)
})
}
)
await check(() => next.fetch('/about').then((res) => res.status), 200)
})
})

View file

@ -1,34 +1,36 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app-dir edge runtime config',
{
describe('app-dir edge runtime config', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
skipDeployment: true,
},
({ next, isNextDev }) => {
it('should warn the legacy object config export', async () => {
let error
await next.start().catch((err) => {
error = err
})
if (isNextDev) {
expect(error).not.toBeDefined()
await next.fetch('/legacy-runtime-config')
} else {
expect(error).toBeDefined()
}
})
expect(next.cliOutput).toContain('Page config in ')
expect(next.cliOutput).toContain(
// the full path is more complex, we only care about this part
'app/legacy-runtime-config/page.js is deprecated. Replace `export const config=…` with the following:'
)
expect(next.cliOutput).toContain('- `export const runtime = "edge"`')
expect(next.cliOutput).toContain(
'- `export const preferredRegion = ["us-east-1"]`'
)
})
if (skipped) {
return
}
)
it('should warn the legacy object config export', async () => {
let error
await next.start().catch((err) => {
error = err
})
if (isNextDev) {
expect(error).not.toBeDefined()
await next.fetch('/legacy-runtime-config')
} else {
expect(error).toBeDefined()
}
expect(next.cliOutput).toContain('Page config in ')
expect(next.cliOutput).toContain(
// the full path is more complex, we only care about this part
'app/legacy-runtime-config/page.js is deprecated. Replace `export const config=…` with the following:'
)
expect(next.cliOutput).toContain('- `export const runtime = "edge"`')
expect(next.cliOutput).toContain(
'- `export const preferredRegion = ["us-east-1"]`'
)
})
})

View file

@ -1,24 +1,21 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'_allow-underscored-root-directory',
{
describe('_allow-underscored-root-directory', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should not serve app path with underscore', async () => {
const res = await next.fetch('/_handlers')
expect(res.status).toBe(404)
})
})
it('should not serve app path with underscore', async () => {
const res = await next.fetch('/_handlers')
expect(res.status).toBe(404)
})
it('should pages path with a underscore at the root', async () => {
const res = await next.fetch('/')
await expect(res.text()).resolves.toBe('Hello, world!')
})
it('should pages path with a underscore at the root', async () => {
const res = await next.fetch('/')
await expect(res.text()).resolves.toBe('Hello, world!')
})
it('should serve app path with %5F', async () => {
const res = await next.fetch('/_routable-folder')
await expect(res.text()).resolves.toBe('Hello, world!')
})
}
)
it('should serve app path with %5F', async () => {
const res = await next.fetch('/_routable-folder')
await expect(res.text()).resolves.toBe('Hello, world!')
})
})

View file

@ -1,10 +1,9 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
import { join } from 'path'
createNextDescribe(
'app-dir action allowed origins',
{
describe('app-dir action allowed origins', () => {
const { next, skipped } = nextTestSetup({
files: join(__dirname, 'safe-origins'),
skipDeployment: true,
dependencies: {
@ -14,16 +13,19 @@ createNextDescribe(
},
// An arbitrary & random port.
forcedPort: '41831',
},
({ next }) => {
it('should pass if localhost is set as a safe origin', async function () {
const browser = await next.browser('/')
})
await browser.elementByCss('button').click()
await check(async () => {
return await browser.elementByCss('#res').text()
}, 'hi')
})
if (skipped) {
return
}
)
it('should pass if localhost is set as a safe origin', async function () {
const browser = await next.browser('/')
await browser.elementByCss('button').click()
await check(async () => {
return await browser.elementByCss('#res').text()
}, 'hi')
})
})

View file

@ -1,10 +1,9 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
import { join } from 'path'
createNextDescribe(
'app-dir action disallowed origins',
{
describe('app-dir action disallowed origins', () => {
const { next, skipped } = nextTestSetup({
files: join(__dirname, 'unsafe-origins'),
skipDeployment: true,
dependencies: {
@ -12,22 +11,25 @@ createNextDescribe(
'react-dom': 'latest',
'server-only': 'latest',
},
},
({ next }) => {
// Origin should be localhost
it('should error if x-forwarded-host does not match the origin', async function () {
const browser = await next.browser('/')
})
await browser.elementByCss('button').click()
await check(async () => {
const t = await browser.elementByCss('#res').text()
return t.includes('Invalid Server Actions request.') ||
// In prod the message is hidden
t.includes('An error occurred in the Server Components render.')
? 'yes'
: 'no'
}, 'yes')
})
if (skipped) {
return
}
)
// Origin should be localhost
it('should error if x-forwarded-host does not match the origin', async function () {
const browser = await next.browser('/')
await browser.elementByCss('button').click()
await check(async () => {
const t = await browser.elementByCss('#res').text()
return t.includes('Invalid Server Actions request.') ||
// In prod the message is hidden
t.includes('An error occurred in the Server Components render.')
? 'yes'
: 'no'
}, 'yes')
})
})

View file

@ -1,49 +1,47 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'
createNextDescribe(
'app-dir action handling',
{
describe('app-dir action handling', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should handle actions correctly after navigation / redirection events', async () => {
const browser = await next.browser('/')
})
await browser.elementByCss('#middleware-redirect').click()
it('should handle actions correctly after navigation / redirection events', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('#form').text()).not.toContain(
'Loading...'
)
await browser.elementByCss('#middleware-redirect').click()
await browser.elementByCss('#submit').click()
expect(await browser.elementByCss('#form').text()).not.toContain(
'Loading...'
)
await check(() => {
return browser.elementByCss('#form').text()
}, /Loading.../)
await browser.elementByCss('#submit').click()
// wait for 2 seconds, since the action takes a second to resolve
await waitFor(2000)
await check(() => {
return browser.elementByCss('#form').text()
}, /Loading.../)
expect(await browser.elementByCss('#form').text()).not.toContain(
'Loading...'
)
// wait for 2 seconds, since the action takes a second to resolve
await waitFor(2000)
expect(await browser.elementByCss('#result').text()).toContain(
'RESULT FROM SERVER ACTION'
)
})
expect(await browser.elementByCss('#form').text()).not.toContain(
'Loading...'
)
it('should handle actions correctly after following a relative link', async () => {
const browser = await next.browser('/nested-folder/products')
expect(await browser.elementByCss('#result').text()).toContain(
'RESULT FROM SERVER ACTION'
)
})
await browser.elementByCss('a').click()
it('should handle actions correctly after following a relative link', async () => {
const browser = await next.browser('/nested-folder/products')
await browser.elementByCss('button').click()
await browser.elementByCss('a').click()
await check(() => {
return (next.cliOutput.match(/addToCart/g) || []).length
}, 1)
})
}
)
await browser.elementByCss('button').click()
await check(() => {
return (next.cliOutput.match(/addToCart/g) || []).length
}, 1)
})
})

View file

@ -1,8 +1,7 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app-dir action handling - next export',
{
describe('app-dir action handling - next export', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
skipDeployment: true,
@ -11,32 +10,32 @@ createNextDescribe(
'react-dom': 'latest',
'server-only': 'latest',
},
},
({ next, isNextStart }) => {
if (!isNextStart) {
it('skip test for development mode', () => {})
return
}
})
if (skipped) return
beforeAll(async () => {
await next.stop()
await next.patchFile(
'next.config.js',
`
if (!isNextStart) {
it('skip test for development mode', () => {})
return
}
beforeAll(async () => {
await next.stop()
await next.patchFile(
'next.config.js',
`
module.exports = {
output: 'export'
}
`
)
try {
await next.start()
} catch {}
})
)
try {
await next.start()
} catch {}
})
it('should error when use export output for server actions', async () => {
expect(next.cliOutput).toContain(
`Server Actions are not supported with static export.`
)
})
}
)
it('should error when use export output for server actions', async () => {
expect(next.cliOutput).toContain(
`Server Actions are not supported with static export.`
)
})
})

View file

@ -1,73 +1,70 @@
/* eslint-disable jest/no-standalone-expect */
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'app-dir action useFormState',
{
describe('app-dir action useFormState', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
react: 'latest',
'react-dom': 'latest',
},
},
({ next }) => {
it('should support submitting form state with JS', async () => {
const browser = await next.browser('/client/form-state')
})
it('should support submitting form state with JS', async () => {
const browser = await next.browser('/client/form-state')
await browser.eval(`document.getElementById('name-input').value = 'test'`)
await browser.elementByCss('#submit-form').click()
await browser.eval(`document.getElementById('name-input').value = 'test'`)
await browser.elementByCss('#submit-form').click()
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test')
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test')
})
it('should support submitting form state without JS', async () => {
const browser = await next.browser('/client/form-state', {
disableJavaScript: true,
})
it('should support submitting form state without JS', async () => {
const browser = await next.browser('/client/form-state', {
disableJavaScript: true,
})
await browser.eval(`document.getElementById('name-input').value = 'test'`)
await browser.elementByCss('#submit-form').click()
await browser.eval(`document.getElementById('name-input').value = 'test'`)
await browser.elementByCss('#submit-form').click()
// It should inline the form state into HTML so it can still be hydrated.
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test')
})
// It should inline the form state into HTML so it can still be hydrated.
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test')
it('should support hydrating the app from progressively enhanced form request', async () => {
const browser = await next.browser('/client/form-state')
// Simulate a progressively enhanced form request
await browser.eval(`document.getElementById('name-input').value = 'test'`)
await browser.eval(`document.getElementById('form-state-form').submit()`)
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test')
// Should hydrate successfully
await check(() => {
return browser.elementByCss('#hydrated').text()
}, 'hydrated')
})
it('should send the action to the provided permalink with form state when JS disabled', async () => {
const browser = await next.browser('/client/form-state/page-2', {
disableJavaScript: true,
})
it('should support hydrating the app from progressively enhanced form request', async () => {
const browser = await next.browser('/client/form-state')
// Simulate a progressively enhanced form request
await browser.eval(
`document.getElementById('name-input').value = 'test-permalink'`
)
await browser.eval(`document.getElementById('form-state-form').submit()`)
// Simulate a progressively enhanced form request
await browser.eval(`document.getElementById('name-input').value = 'test'`)
await browser.eval(`document.getElementById('form-state-form').submit()`)
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test')
// Should hydrate successfully
await check(() => {
return browser.elementByCss('#hydrated').text()
}, 'hydrated')
})
it('should send the action to the provided permalink with form state when JS disabled', async () => {
const browser = await next.browser('/client/form-state/page-2', {
disableJavaScript: true,
})
// Simulate a progressively enhanced form request
await browser.eval(
`document.getElementById('name-input').value = 'test-permalink'`
)
await browser.eval(`document.getElementById('form-state-form').submit()`)
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test-permalink')
})
}
)
await check(() => {
return browser.elementByCss('#form-state').text()
}, 'initial-state:test-permalink')
})
})

View file

@ -1,11 +1,10 @@
/* eslint-disable jest/no-standalone-expect */
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
import type { Response } from 'playwright'
createNextDescribe(
'app-dir action progressive enhancement',
{
describe('app-dir action progressive enhancement', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
react: 'latest',
@ -13,46 +12,43 @@ createNextDescribe(
'react-dom': 'latest',
'server-only': 'latest',
},
},
({ next }) => {
it('should support formData and redirect without JS', async () => {
let responseCode
const browser = await next.browser('/server', {
disableJavaScript: true,
beforePageLoad(page) {
page.on('response', (response: Response) => {
const url = new URL(response.url())
const status = response.status()
if (url.pathname.includes('/server')) {
responseCode = status
}
})
},
})
})
await browser.eval(`document.getElementById('name').value = 'test'`)
await browser.elementByCss('#submit').click()
await check(() => {
return browser.eval('window.location.pathname + window.location.search')
}, '/header?name=test&hidden-info=hi')
expect(responseCode).toBe(303)
it('should support formData and redirect without JS', async () => {
let responseCode
const browser = await next.browser('/server', {
disableJavaScript: true,
beforePageLoad(page) {
page.on('response', (response: Response) => {
const url = new URL(response.url())
const status = response.status()
if (url.pathname.includes('/server')) {
responseCode = status
}
})
},
})
it('should support actions from client without JS', async () => {
const browser = await next.browser('/server', {
disableJavaScript: true,
})
await browser.eval(`document.getElementById('name').value = 'test'`)
await browser.elementByCss('#submit').click()
await browser.eval(
`document.getElementById('client-name').value = 'test'`
)
await browser.elementByCss('#there').click()
await check(() => {
return browser.eval('window.location.pathname + window.location.search')
}, '/header?name=test&hidden-info=hi')
await check(() => {
return browser.eval('window.location.pathname + window.location.search')
}, '/header?name=test&hidden-info=hi')
expect(responseCode).toBe(303)
})
it('should support actions from client without JS', async () => {
const browser = await next.browser('/server', {
disableJavaScript: true,
})
}
)
await browser.eval(`document.getElementById('client-name').value = 'test'`)
await browser.elementByCss('#there').click()
await check(() => {
return browser.eval('window.location.pathname + window.location.search')
}, '/header?name=test&hidden-info=hi')
})
})

View file

@ -1,13 +1,12 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import { accountForOverhead } from './account-for-overhead'
const CONFIG_ERROR =
'Server Actions Size Limit must be a valid number or filesize format larger than 1MB'
createNextDescribe(
'app-dir action size limit invalid config',
{
describe('app-dir action size limit invalid config', () => {
const { next, isNextStart, isNextDeploy, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
skipStart: true,
@ -16,143 +15,143 @@ createNextDescribe(
'react-dom': 'latest',
'server-only': 'latest',
},
},
({ next, isNextStart, isNextDeploy }) => {
if (!isNextStart) {
it('skip test for development mode', () => {})
return
})
if (skipped) return
if (!isNextStart) {
it('skip test for development mode', () => {})
return
}
const logs: string[] = []
beforeAll(() => {
const onLog = (log: string) => {
logs.push(log.trim())
}
const logs: string[] = []
next.on('stdout', onLog)
next.on('stderr', onLog)
})
beforeAll(() => {
const onLog = (log: string) => {
logs.push(log.trim())
}
afterEach(async () => {
logs.length = 0
next.on('stdout', onLog)
next.on('stderr', onLog)
})
await next.stop()
})
afterEach(async () => {
logs.length = 0
await next.stop()
})
it('should error if serverActions.bodySizeLimit config is a negative number', async function () {
await next.patchFile(
'next.config.js',
`
it('should error if serverActions.bodySizeLimit config is a negative number', async function () {
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: { bodySizeLimit: -3000 }
},
}
`
)
try {
await next.start()
} catch {}
expect(next.cliOutput).toContain(CONFIG_ERROR)
})
)
try {
await next.start()
} catch {}
expect(next.cliOutput).toContain(CONFIG_ERROR)
})
it('should error if serverActions.bodySizeLimit config is invalid', async function () {
await next.patchFile(
'next.config.js',
`
it('should error if serverActions.bodySizeLimit config is invalid', async function () {
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: { bodySizeLimit: 'testmb' }
},
}
`
)
try {
await next.start()
} catch {}
expect(next.cliOutput).toContain(CONFIG_ERROR)
})
)
try {
await next.start()
} catch {}
expect(next.cliOutput).toContain(CONFIG_ERROR)
})
it('should error if serverActions.bodySizeLimit config is a negative size', async function () {
await next.patchFile(
'next.config.js',
`
it('should error if serverActions.bodySizeLimit config is a negative size', async function () {
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: { bodySizeLimit: '-3000mb' }
},
}
`
)
try {
await next.start()
} catch {}
expect(next.cliOutput).toContain(CONFIG_ERROR)
})
)
try {
await next.start()
} catch {}
expect(next.cliOutput).toContain(CONFIG_ERROR)
})
if (!isNextDeploy) {
it('should respect the size set in serverActions.bodySizeLimit', async function () {
await next.patchFile(
'next.config.js',
`
if (!isNextDeploy) {
it('should respect the size set in serverActions.bodySizeLimit', async function () {
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: { bodySizeLimit: '1.5mb' }
},
}
`
)
await next.start()
)
await next.start()
const browser = await next.browser('/file')
await browser.elementByCss('#size-1mb').click()
const browser = await next.browser('/file')
await browser.elementByCss('#size-1mb').click()
await retry(() => {
expect(logs).toContainEqual(`size = ${accountForOverhead(1)}`)
})
await browser.elementByCss('#size-2mb').click()
await retry(() => {
expect(logs).toContainEqual(
expect.stringContaining('[Error]: Body exceeded 1.5mb limit')
)
expect(logs).toContainEqual(
expect.stringContaining(
'To configure the body size limit for Server Actions, see'
)
)
})
await retry(() => {
expect(logs).toContainEqual(`size = ${accountForOverhead(1)}`)
})
it('should respect the size set in serverActions.bodySizeLimit when submitting form', async function () {
await next.patchFile(
'next.config.js',
`
await browser.elementByCss('#size-2mb').click()
await retry(() => {
expect(logs).toContainEqual(
expect.stringContaining('[Error]: Body exceeded 1.5mb limit')
)
expect(logs).toContainEqual(
expect.stringContaining(
'To configure the body size limit for Server Actions, see'
)
)
})
})
it('should respect the size set in serverActions.bodySizeLimit when submitting form', async function () {
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: { bodySizeLimit: '2mb' }
},
}
`
)
)
await next.start()
await next.start()
const browser = await next.browser('/form')
await browser.elementByCss('#size-1mb').click()
const browser = await next.browser('/form')
await browser.elementByCss('#size-1mb').click()
await retry(() => {
expect(logs).toContainEqual(`size = ${accountForOverhead(1)}`)
})
await browser.elementByCss('#size-2mb').click()
await retry(() => {
expect(logs).toContainEqual(`size = ${accountForOverhead(2)}`)
})
await retry(() => {
expect(logs).toContainEqual(`size = ${accountForOverhead(1)}`)
})
}
await browser.elementByCss('#size-2mb').click()
await retry(() => {
expect(logs).toContainEqual(`size = ${accountForOverhead(2)}`)
})
})
}
)
})

File diff suppressed because it is too large Load diff

View file

@ -1,44 +1,46 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
import type { BrowserInterface } from 'next-webdriver'
createNextDescribe(
'app a11y features',
{
describe('app a11y features', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
packageJson: {},
skipDeployment: true,
},
({ next }) => {
describe('route announcer', () => {
async function getAnnouncerContent(browser: BrowserInterface) {
return browser.eval(
`document.getElementsByTagName('next-route-announcer')[0]?.shadowRoot.childNodes[0]?.innerHTML`
)
}
})
it('should not announce the initital title', async () => {
const browser = await next.browser('/page-with-h1')
await check(() => getAnnouncerContent(browser), '')
})
it('should announce document.title changes', async () => {
const browser = await next.browser('/page-with-h1')
await browser.elementById('page-with-title').click()
await check(() => getAnnouncerContent(browser), 'page-with-title')
})
it('should announce h1 changes', async () => {
const browser = await next.browser('/page-with-h1')
await browser.elementById('noop-layout-page-1').click()
await check(() => getAnnouncerContent(browser), 'noop-layout/page-1')
})
it('should announce route changes when h1 changes inside an inner layout', async () => {
const browser = await next.browser('/noop-layout/page-1')
await browser.elementById('noop-layout-page-2').click()
await check(() => getAnnouncerContent(browser), 'noop-layout/page-2')
})
})
if (skipped) {
return
}
)
describe('route announcer', () => {
async function getAnnouncerContent(browser: BrowserInterface) {
return browser.eval(
`document.getElementsByTagName('next-route-announcer')[0]?.shadowRoot.childNodes[0]?.innerHTML`
)
}
it('should not announce the initital title', async () => {
const browser = await next.browser('/page-with-h1')
await check(() => getAnnouncerContent(browser), '')
})
it('should announce document.title changes', async () => {
const browser = await next.browser('/page-with-h1')
await browser.elementById('page-with-title').click()
await check(() => getAnnouncerContent(browser), 'page-with-title')
})
it('should announce h1 changes', async () => {
const browser = await next.browser('/page-with-h1')
await browser.elementById('noop-layout-page-1').click()
await check(() => getAnnouncerContent(browser), 'noop-layout/page-1')
})
it('should announce route changes when h1 changes inside an inner layout', async () => {
const browser = await next.browser('/noop-layout/page-1')
await browser.elementById('noop-layout-page-2').click()
await check(() => getAnnouncerContent(browser), 'noop-layout/page-2')
})
})
})

View file

@ -1,46 +1,48 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app-dir alias',
{
describe('app-dir alias', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextStart }) => {
it('should handle typescript paths alias correctly', async () => {
const html = await next.render('/button')
expect(html).toContain('click</button>')
})
})
it('should resolve css imports from outside with src folder presented', async () => {
const browser = await next.browser('/button')
const fontSize = await browser
.elementByCss('button')
.getComputedCss('font-size')
expect(fontSize).toBe('50px')
})
if (isNextStart) {
it('should not contain installed react/react-dom version in client chunks', async () => {
const appBuildManifest = await next.readJSON(
'.next/app-build-manifest.json'
)
Object.keys(appBuildManifest.pages).forEach((page) => {
const containFrameworkChunk = appBuildManifest.pages[page].some(
(chunk) => {
return chunk.includes('framework')
}
)
expect(containFrameworkChunk).toBe(false)
})
})
it('should generate app-build-manifest correctly', async () => {
// Remove other page CSS files:
const manifest = await next.readJSON('.next/app-build-manifest.json')
expect(manifest.pages).not.toBeEmptyObject()
})
}
if (skipped) {
return
}
)
it('should handle typescript paths alias correctly', async () => {
const html = await next.render('/button')
expect(html).toContain('click</button>')
})
it('should resolve css imports from outside with src folder presented', async () => {
const browser = await next.browser('/button')
const fontSize = await browser
.elementByCss('button')
.getComputedCss('font-size')
expect(fontSize).toBe('50px')
})
if (isNextStart) {
it('should not contain installed react/react-dom version in client chunks', async () => {
const appBuildManifest = await next.readJSON(
'.next/app-build-manifest.json'
)
Object.keys(appBuildManifest.pages).forEach((page) => {
const containFrameworkChunk = appBuildManifest.pages[page].some(
(chunk) => {
return chunk.includes('framework')
}
)
expect(containFrameworkChunk).toBe(false)
})
})
it('should generate app-build-manifest correctly', async () => {
// Remove other page CSS files:
const manifest = await next.readJSON('.next/app-build-manifest.json')
expect(manifest.pages).not.toBeEmptyObject()
})
}
})

View file

@ -1,60 +1,62 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check, retry } from 'next-test-utils'
import { join } from 'path'
createNextDescribe(
'custom-app-server-action-redirect',
{
describe('custom-app-server-action-redirect', () => {
const { next, skipped } = nextTestSetup({
files: join(__dirname, 'custom-server'),
skipDeployment: true,
startCommand: 'node server.js',
dependencies: {
'get-port': '5.1.1',
},
},
({ next }) => {
it('redirects with basepath properly when server action handler uses `redirect`', async () => {
const browser = await next.browser('/base')
const getCount = async () => browser.elementByCss('#current-count').text()
})
// Increase count to track if the page reloaded
await browser.elementByCss('#increase-count').click().click()
await retry(async () => {
expect(await getCount()).toBe('Count: 2')
})
if (skipped) {
return
}
await browser.elementById('submit-server-action-redirect').click()
it('redirects with basepath properly when server action handler uses `redirect`', async () => {
const browser = await next.browser('/base')
const getCount = async () => browser.elementByCss('#current-count').text()
expect(await browser.waitForElementByCss('#another').text()).toBe(
'Another Page'
)
expect(await browser.url()).toBe(
`http://localhost:${next.appPort}/base/another`
)
// Count should still be 2 as the browser should not have reloaded the page.
// Increase count to track if the page reloaded
await browser.elementByCss('#increase-count').click().click()
await retry(async () => {
expect(await getCount()).toBe('Count: 2')
})
it('redirects with proper cookies set from both redirect response and post respose', async () => {
const browser = await next.browser('/base')
await browser.elementById('submit-server-action-redirect').click()
await browser.elementById('submit-server-action-redirect').click()
expect(await browser.waitForElementByCss('#another').text()).toBe(
'Another Page'
)
expect(await browser.url()).toBe(
`http://localhost:${next.appPort}/base/another`
)
expect(await browser.waitForElementByCss('#another').text()).toBe(
'Another Page'
)
expect(await browser.url()).toBe(
`http://localhost:${next.appPort}/base/another`
)
await check(
() => browser.eval('document.cookie'),
/custom-server-test-cookie/
)
await check(
() => browser.eval('document.cookie'),
/custom-server-action-test-cookie/
)
})
}
)
// Count should still be 2 as the browser should not have reloaded the page.
expect(await getCount()).toBe('Count: 2')
})
it('redirects with proper cookies set from both redirect response and post respose', async () => {
const browser = await next.browser('/base')
await browser.elementById('submit-server-action-redirect').click()
expect(await browser.waitForElementByCss('#another').text()).toBe(
'Another Page'
)
expect(await browser.url()).toBe(
`http://localhost:${next.appPort}/base/another`
)
await check(
() => browser.eval('document.cookie'),
/custom-server-test-cookie/
)
await check(
() => browser.eval('document.cookie'),
/custom-server-action-test-cookie/
)
})
})

View file

@ -1,95 +1,97 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
createNextDescribe(
'app dir - basepath',
{
describe('app dir - basepath', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
dependencies: {
sass: 'latest',
},
},
({ next }) => {
it('should successfully hard navigate from pages -> app', async () => {
const browser = await next.browser('/base/pages-path')
await browser.elementByCss('#to-another').click()
await browser.waitForElementByCss('#page-2')
})
})
it('should support `basePath`', async () => {
const html = await next.render('/base')
expect(html).toContain('<h1>Test Page</h1>')
})
it('should support Link with basePath prefixed', async () => {
const browser = await next.browser('/base')
expect(
await browser
.elementByCss('a[href="/base/another"]')
.click()
.waitForElementByCss('#page-2')
.text()
).toBe(`Page 2`)
})
it('should prefix metadata og image with basePath', async () => {
const $ = await next.render$('/base/another')
const ogImageHref = $('meta[property="og:image"]').attr('content')
expect(ogImageHref).toContain('/base/another/opengraph-image.png')
})
it('should prefix redirect() with basePath', async () => {
const browser = await next.browser('/base/redirect')
await retry(async () => {
expect(await browser.url()).toBe(`${next.url}/base/another`)
})
})
it('should render usePathname without the basePath', async () => {
const pathnames = ['/use-pathname', '/use-pathname-another']
const validatorPromises = pathnames.map(async (pathname) => {
const $ = await next.render$('/base' + pathname)
expect($('#pathname').data('pathname')).toBe(pathname)
})
await Promise.all(validatorPromises)
})
it('should handle redirect in dynamic in suspense boundary routes with basePath', async () => {
const browser = await next.browser('/base/dynamic/source')
await retry(async () => {
// Check content is loaded first to avoid flakiness
expect(await browser.elementByCss('p').text()).toBe(`id:dest`)
expect(await browser.url()).toBe(`${next.url}/base/dynamic/dest`)
})
})
it.each(['/base/refresh', '/base/refresh?foo=bar'])(
`should only make a single RSC call to the current page (%s)`,
async (path) => {
let rscRequests = []
const browser = await next.browser(path, {
beforePageLoad(page) {
page.on('request', (request) => {
return request.allHeaders().then((headers) => {
if (
headers['RSC'.toLowerCase()] === '1' &&
// Prefetches also include `RSC`
headers['Next-Router-Prefetch'.toLowerCase()] !== '1'
) {
rscRequests.push(request.url())
}
})
})
},
})
await browser.elementByCss('button').click()
await retry(async () => {
expect(rscRequests.length).toBe(1)
expect(rscRequests[0]).toContain(`${next.url}${path}`)
})
}
)
if (skipped) {
return
}
)
it('should successfully hard navigate from pages -> app', async () => {
const browser = await next.browser('/base/pages-path')
await browser.elementByCss('#to-another').click()
await browser.waitForElementByCss('#page-2')
})
it('should support `basePath`', async () => {
const html = await next.render('/base')
expect(html).toContain('<h1>Test Page</h1>')
})
it('should support Link with basePath prefixed', async () => {
const browser = await next.browser('/base')
expect(
await browser
.elementByCss('a[href="/base/another"]')
.click()
.waitForElementByCss('#page-2')
.text()
).toBe(`Page 2`)
})
it('should prefix metadata og image with basePath', async () => {
const $ = await next.render$('/base/another')
const ogImageHref = $('meta[property="og:image"]').attr('content')
expect(ogImageHref).toContain('/base/another/opengraph-image.png')
})
it('should prefix redirect() with basePath', async () => {
const browser = await next.browser('/base/redirect')
await retry(async () => {
expect(await browser.url()).toBe(`${next.url}/base/another`)
})
})
it('should render usePathname without the basePath', async () => {
const pathnames = ['/use-pathname', '/use-pathname-another']
const validatorPromises = pathnames.map(async (pathname) => {
const $ = await next.render$('/base' + pathname)
expect($('#pathname').data('pathname')).toBe(pathname)
})
await Promise.all(validatorPromises)
})
it('should handle redirect in dynamic in suspense boundary routes with basePath', async () => {
const browser = await next.browser('/base/dynamic/source')
await retry(async () => {
// Check content is loaded first to avoid flakiness
expect(await browser.elementByCss('p').text()).toBe(`id:dest`)
expect(await browser.url()).toBe(`${next.url}/base/dynamic/dest`)
})
})
it.each(['/base/refresh', '/base/refresh?foo=bar'])(
`should only make a single RSC call to the current page (%s)`,
async (path) => {
let rscRequests = []
const browser = await next.browser(path, {
beforePageLoad(page) {
page.on('request', (request) => {
return request.allHeaders().then((headers) => {
if (
headers['RSC'.toLowerCase()] === '1' &&
// Prefetches also include `RSC`
headers['Next-Router-Prefetch'.toLowerCase()] !== '1'
) {
rscRequests.push(request.url())
}
})
})
},
})
await browser.elementByCss('button').click()
await retry(async () => {
expect(rscRequests.length).toBe(1)
expect(rscRequests[0]).toContain(`${next.url}${path}`)
})
}
)
})

View file

@ -1,4 +1,4 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
import { BrowserInterface } from 'next-webdriver'
import {
@ -8,20 +8,308 @@ import {
getPathname,
} from './test-utils'
createNextDescribe(
'app dir client cache semantics',
{
describe('app dir client cache semantics', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
},
({ next, isNextDev }) => {
if (isNextDev) {
// dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is reused for 30s
it('should renew the 30s cache once the data is revalidated', async () => {
let browser = (await next.browser(
})
if (isNextDev) {
// dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is reused for 30s
it('should renew the 30s cache once the data is revalidated', async () => {
let browser = (await next.browser(
'/',
browserConfigWithFixedTime
)) as BrowserInterface
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
let initialNumber = await browser.elementById('random-number').text()
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/1"]').click()
let newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s.
expect(newNumber).toBe(initialNumber)
// Fast forward to expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.elementByCss('[href="/1"]').click()
newNumber = await browser.elementById('random-number').text()
// ~35s have passed, so the cache should be expired and the number should be different
expect(newNumber).not.toBe(initialNumber)
// once the number is updated, we should have a renewed 30s cache for this entry
// store this new number so we can check that it stays the same
initialNumber = newNumber
await browser.eval(fastForwardTo, 5 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.elementByCss('[href="/1"]').click()
newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})
} else {
describe('prefetch={true}', () => {
let browser: BrowserInterface
beforeEach(async () => {
browser = (await next.browser(
'/',
browserConfigWithFixedTime
)) as BrowserInterface
})
it('should prefetch the full page', async () => {
const { getRequests, clearRequests } = await createRequestsListener(
browser
)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
clearRequests()
await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().every(([url]) => getPathname(url) !== '/0')
).toEqual(true)
})
it('should re-use the cache for the full page, only for 5 mins', async () => {
const randomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
})
it('should prefetch again after 5 mins if the link is visible again', async () => {
const { getRequests, clearRequests } = await createRequestsListener(
browser
)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
const randomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
clearRequests()
await browser.elementByCss('[href="/"]').click()
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
const number = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).not.toBe(randomNumber)
})
})
describe('prefetch={false}', () => {
let browser: BrowserInterface
beforeEach(async () => {
browser = (await next.browser(
'/',
browserConfigWithFixedTime
)) as BrowserInterface
})
it('should not prefetch the page at all', async () => {
const { getRequests } = await createRequestsListener(browser)
await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().filter(([url]) => getPathname(url) === '/2')
).toHaveLength(1)
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/2' && didPartialPrefetch
)
).toBe(false)
})
it('should re-use the cache only for 30 seconds', async () => {
const randomNumber = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
})
})
describe('prefetch={undefined} - default', () => {
let browser: BrowserInterface
beforeEach(async () => {
browser = (await next.browser(
'/',
browserConfigWithFixedTime
)) as BrowserInterface
})
it('should prefetch partially a dynamic page', async () => {
const { getRequests, clearRequests } = await createRequestsListener(
browser
)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
clearRequests()
await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && !didPartialPrefetch
)
).toBe(true)
})
it('should re-use the full cache for only 30 seconds', async () => {
const randomNumber = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber2 = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber2).not.toBe(newNumber)
})
it('should renew the 30s cache once the data is revalidated', async () => {
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
@ -64,416 +352,124 @@ createNextDescribe(
// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})
} else {
describe('prefetch={true}', () => {
let browser: BrowserInterface
beforeEach(async () => {
browser = (await next.browser(
'/',
browserConfigWithFixedTime
)) as BrowserInterface
})
it('should refetch below the fold after 30 seconds', async () => {
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
it('should prefetch the full page', async () => {
const { getRequests, clearRequests } = await createRequestsListener(
browser
)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
clearRequests()
await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().every(([url]) => getPathname(url) !== '/0')
).toEqual(true)
})
it('should re-use the cache for the full page, only for 5 mins', async () => {
const randomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
})
it('should prefetch again after 5 mins if the link is visible again', async () => {
const { getRequests, clearRequests } = await createRequestsListener(
browser
)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
const randomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
clearRequests()
await browser.elementByCss('[href="/"]').click()
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
const number = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).not.toBe(randomNumber)
})
})
describe('prefetch={false}', () => {
let browser: BrowserInterface
beforeEach(async () => {
browser = (await next.browser(
'/',
browserConfigWithFixedTime
)) as BrowserInterface
})
it('should not prefetch the page at all', async () => {
const { getRequests } = await createRequestsListener(browser)
await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().filter(([url]) => getPathname(url) === '/2')
).toHaveLength(1)
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/2' && didPartialPrefetch
)
).toBe(false)
})
it('should re-use the cache only for 30 seconds', async () => {
const randomNumber = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
})
})
describe('prefetch={undefined} - default', () => {
let browser: BrowserInterface
beforeEach(async () => {
browser = (await next.browser(
'/',
browserConfigWithFixedTime
)) as BrowserInterface
})
it('should prefetch partially a dynamic page', async () => {
const { getRequests, clearRequests } = await createRequestsListener(
browser
)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
clearRequests()
await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && !didPartialPrefetch
)
).toBe(true)
})
it('should re-use the full cache for only 30 seconds', async () => {
const randomNumber = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber2 = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber2).not.toBe(newNumber)
})
it('should renew the 30s cache once the data is revalidated', async () => {
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
let initialNumber = await browser.elementById('random-number').text()
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/1"]').click()
let newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s.
expect(newNumber).toBe(initialNumber)
// Fast forward to expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.elementByCss('[href="/1"]').click()
newNumber = await browser.elementById('random-number').text()
// ~35s have passed, so the cache should be expired and the number should be different
expect(newNumber).not.toBe(initialNumber)
// once the number is updated, we should have a renewed 30s cache for this entry
// store this new number so we can check that it stays the same
initialNumber = newNumber
await browser.eval(fastForwardTo, 5 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.elementByCss('[href="/1"]').click()
newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})
it('should refetch below the fold after 30 seconds', async () => {
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
await browser.eval(fastForwardTo, 30 * 1000)
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
it('should refetch the full page after 5 mins', async () => {
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser
.elementByCss('[href="/"]')
.click()
.waitForElementByCss('[href="/1?timeout=1000"]')
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).not.toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
it('should respect a loading boundary that returns `null`', async () => {
await browser.elementByCss('[href="/null-loading"]').click()
// the page content should disappear immediately
expect(
await browser.hasElementByCssSelector('[href="/null-loading"]')
).toBeFalse()
// the root layout should still be visible
expect(
await browser.hasElementByCssSelector('#root-layout')
).toBeTrue()
// the dynamic content should eventually appear
await browser.waitForElementByCss('#random-number')
expect(
await browser.hasElementByCssSelector('#random-number')
).toBeTrue()
})
})
it('should seed the prefetch cache with the fetched page data', async () => {
const browser = (await next.browser(
'/1',
browserConfigWithFixedTime
)) as BrowserInterface
const initialNumber = await browser.elementById('random-number').text()
// Move forward a few seconds, navigate off the page and then back to it
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.elementByCss('[href="/1"]').click()
const newNumber = await browser.elementById('random-number').text()
// The number should be the same as we've seeded it in the prefetch cache when we loaded the full page
expect(newNumber).toBe(initialNumber)
})
it('should renew the initial seeded data after expiration time', async () => {
const browser = (await next.browser(
'/without-loading/1',
browserConfigWithFixedTime
)) as BrowserInterface
const initialNumber = await browser.elementById('random-number').text()
// Expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/without-loading"]').click()
await browser.elementByCss('[href="/without-loading/1"]').click()
const newNumber = await browser.elementById('random-number').text()
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
// The number should be different, as the seeded data has expired after 30s
expect(newNumber).not.toBe(initialNumber)
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
}
it('should refetch the full page after 5 mins', async () => {
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser
.elementByCss('[href="/"]')
.click()
.waitForElementByCss('[href="/1?timeout=1000"]')
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).not.toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
it('should respect a loading boundary that returns `null`', async () => {
await browser.elementByCss('[href="/null-loading"]').click()
// the page content should disappear immediately
expect(
await browser.hasElementByCssSelector('[href="/null-loading"]')
).toBeFalse()
// the root layout should still be visible
expect(await browser.hasElementByCssSelector('#root-layout')).toBeTrue()
// the dynamic content should eventually appear
await browser.waitForElementByCss('#random-number')
expect(
await browser.hasElementByCssSelector('#random-number')
).toBeTrue()
})
})
it('should seed the prefetch cache with the fetched page data', async () => {
const browser = (await next.browser(
'/1',
browserConfigWithFixedTime
)) as BrowserInterface
const initialNumber = await browser.elementById('random-number').text()
// Move forward a few seconds, navigate off the page and then back to it
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.elementByCss('[href="/1"]').click()
const newNumber = await browser.elementById('random-number').text()
// The number should be the same as we've seeded it in the prefetch cache when we loaded the full page
expect(newNumber).toBe(initialNumber)
})
it('should renew the initial seeded data after expiration time', async () => {
const browser = (await next.browser(
'/without-loading/1',
browserConfigWithFixedTime
)) as BrowserInterface
const initialNumber = await browser.elementById('random-number').text()
// Expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/without-loading"]').click()
await browser.elementByCss('[href="/without-loading/1"]').click()
const newNumber = await browser.elementById('random-number').text()
// The number should be different, as the seeded data has expired after 30s
expect(newNumber).not.toBe(initialNumber)
})
}
)
})

View file

@ -1,52 +1,50 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check, hasRedbox, waitFor } from 'next-test-utils'
createNextDescribe(
'app dir',
{
describe('app dir', () => {
const { next, isNextDev, isNextStart } = nextTestSetup({
files: __dirname,
},
({ next, isNextDev: isDev, isNextStart }) => {
if (isNextStart) {
describe('Loading', () => {
it('should render loading.js in initial html for slow page', async () => {
const $ = await next.render$('/page-with-loading')
expect($('#loading').text()).toBe('Loading...')
})
})
if (isNextStart) {
describe('Loading', () => {
it('should render loading.js in initial html for slow page', async () => {
const $ = await next.render$('/page-with-loading')
expect($('#loading').text()).toBe('Loading...')
})
}
if (isDev) {
describe('HMR', () => {
it('should not cause error when removing loading.js', async () => {
const browser = await next.browser('/page-with-loading')
await check(
() => browser.elementByCss('h1').text(),
'hello from slow page'
)
await next.renameFile(
'app/page-with-loading/loading.js',
'app/page-with-loading/_loading.js'
)
await waitFor(1000)
// It should not have an error
expect(await hasRedbox(browser)).toBe(false)
// HMR should still work
const code = await next.readFile('app/page-with-loading/page.js')
await next.patchFile(
'app/page-with-loading/page.js',
code.replace('hello from slow page', 'hello from new page')
)
await check(
() => browser.elementByCss('h1').text(),
'hello from new page'
)
})
})
}
})
}
)
if (isNextDev) {
describe('HMR', () => {
it('should not cause error when removing loading.js', async () => {
const browser = await next.browser('/page-with-loading')
await check(
() => browser.elementByCss('h1').text(),
'hello from slow page'
)
await next.renameFile(
'app/page-with-loading/loading.js',
'app/page-with-loading/_loading.js'
)
await waitFor(1000)
// It should not have an error
expect(await hasRedbox(browser)).toBe(false)
// HMR should still work
const code = await next.readFile('app/page-with-loading/page.js')
await next.patchFile(
'app/page-with-loading/page.js',
code.replace('hello from slow page', 'hello from new page')
)
await check(
() => browser.elementByCss('h1').text(),
'hello from new page'
)
})
})
}
})

View file

@ -1,37 +1,39 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - crossOrigin config',
{
describe('app dir - crossOrigin config', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextStart }) => {
if (isNextStart) {
it('skip in start mode', () => {})
return
}
it('should render correctly with assetPrefix: "/"', async () => {
const $ = await next.render$('/')
// Only potential external (assetPrefix) <script /> and <link /> should have crossorigin attribute
$(
'script[src*="https://example.vercel.sh"], link[href*="https://example.vercel.sh"]'
).each((_, el) => {
const crossOrigin = $(el).attr('crossorigin')
expect(crossOrigin).toBe('use-credentials')
})
})
// Inline <script /> (including RSC payload) and <link /> should not have crossorigin attribute
$('script:not([src]), link:not([href])').each((_, el) => {
const crossOrigin = $(el).attr('crossorigin')
expect(crossOrigin).toBeUndefined()
})
// Same origin <script /> and <link /> should not have crossorigin attribute either
$('script[src^="/"], link[href^="/"]').each((_, el) => {
const crossOrigin = $(el).attr('crossorigin')
expect(crossOrigin).toBeUndefined()
})
})
if (skipped) {
return
}
)
if (isNextStart) {
it('skip in start mode', () => {})
return
}
it('should render correctly with assetPrefix: "/"', async () => {
const $ = await next.render$('/')
// Only potential external (assetPrefix) <script /> and <link /> should have crossorigin attribute
$(
'script[src*="https://example.vercel.sh"], link[href*="https://example.vercel.sh"]'
).each((_, el) => {
const crossOrigin = $(el).attr('crossorigin')
expect(crossOrigin).toBe('use-credentials')
})
// Inline <script /> (including RSC payload) and <link /> should not have crossorigin attribute
$('script:not([src]), link:not([href])').each((_, el) => {
const crossOrigin = $(el).attr('crossorigin')
expect(crossOrigin).toBeUndefined()
})
// Same origin <script /> and <link /> should not have crossorigin attribute either
$('script[src^="/"], link[href^="/"]').each((_, el) => {
const crossOrigin = $(el).attr('crossorigin')
expect(crossOrigin).toBeUndefined()
})
})
})

View file

@ -1,8 +1,7 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - css with pageextensions',
{
describe('app dir - css with pageextensions', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
dependencies: {
@ -11,19 +10,22 @@ createNextDescribe(
'react-dom': 'latest',
sass: 'latest',
},
},
({ next }) => {
describe('css support with pageextensions', () => {
describe('page in app directory with pageextention, css should work', () => {
it('should support global css inside layout', async () => {
const browser = await next.browser('/css-pageextensions')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
)
).toBe('rgb(255, 0, 0)')
})
})
if (skipped) {
return
}
describe('css support with pageextensions', () => {
describe('page in app directory with pageextention, css should work', () => {
it('should support global css inside layout', async () => {
const browser = await next.browser('/css-pageextensions')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
)
).toBe('rgb(255, 0, 0)')
})
})
}
)
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import { type NextInstance, createNextDescribe, FileRef } from 'e2e-utils'
import { type NextInstance, nextTestSetup, FileRef } from 'e2e-utils'
import { check } from 'next-test-utils'
import fs from 'fs'
@ -27,37 +27,40 @@ function runTests(
})
}
createNextDescribe(
'app-dir - custom-cache-handler - cjs',
{
describe('app-dir - custom-cache-handler - cjs', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
env: {
CUSTOM_CACHE_HANDLER: 'cache-handler.js',
},
},
({ next, isNextDev }) => {
runTests('cjs module exports', { next, isNextDev })
}
)
})
createNextDescribe(
'app-dir - custom-cache-handler - cjs-default-export',
{
if (skipped) {
return
}
runTests('cjs module exports', { next, isNextDev })
})
describe('app-dir - custom-cache-handler - cjs-default-export', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
env: {
CUSTOM_CACHE_HANDLER: 'cache-handler-cjs-default-export.js',
},
},
({ next, isNextDev }) => {
runTests('cjs default export', { next, isNextDev })
}
)
})
createNextDescribe(
'app-dir - custom-cache-handler - esm',
{
if (skipped) {
return
}
runTests('cjs default export', { next, isNextDev })
})
describe('app-dir - custom-cache-handler - esm', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: {
app: new FileRef(__dirname + '/app'),
'cache-handler-esm.js': new FileRef(__dirname + '/cache-handler-esm.js'),
@ -73,8 +76,11 @@ createNextDescribe(
env: {
CUSTOM_CACHE_HANDLER: 'cache-handler-esm.js',
},
},
({ next, isNextDev }) => {
runTests('esm default export', { next, isNextDev })
})
if (skipped) {
return
}
)
runTests('esm default export', { next, isNextDev })
})

View file

@ -1,29 +1,31 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app-dir edge runtime root layout',
{
describe('app-dir edge runtime root layout', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextStart }) => {
it('should not emit metadata files into bad paths', async () => {
await next.fetch('/favicon.ico')
// issue: If metadata files are not filter out properly with image-loader,
// an incorrect static/media folder will be generated
})
// Check that the static folder is not generated
const incorrectGeneratedStaticFolder = await next.hasFile('static')
expect(incorrectGeneratedStaticFolder).toBe(false)
})
if (isNextStart) {
it('should mark static contain metadata routes as edge functions', async () => {
const middlewareManifest = await next.readFile(
'.next/server/middleware-manifest.json'
)
expect(middlewareManifest).not.toContain('favicon')
})
}
if (skipped) {
return
}
)
it('should not emit metadata files into bad paths', async () => {
await next.fetch('/favicon.ico')
// issue: If metadata files are not filter out properly with image-loader,
// an incorrect static/media folder will be generated
// Check that the static folder is not generated
const incorrectGeneratedStaticFolder = await next.hasFile('static')
expect(incorrectGeneratedStaticFolder).toBe(false)
})
if (isNextStart) {
it('should mark static contain metadata routes as edge functions', async () => {
const middlewareManifest = await next.readFile(
'.next/server/middleware-manifest.json'
)
expect(middlewareManifest).not.toContain('favicon')
})
}
})

View file

@ -1,108 +1,110 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'app-dir edge SSR',
{
describe('app-dir edge SSR', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('should handle edge only routes', async () => {
const appHtml = await next.render('/edge/basic')
expect(appHtml).toContain('<p>Edge!</p>')
})
const pageHtml = await next.render('/pages-edge')
expect(pageHtml).toContain('<p>pages-edge-ssr</p>')
})
it('should retrieve cookies in a server component in the edge runtime', async () => {
const res = await next.fetch('/edge-apis/cookies')
expect(await res.text()).toInclude('Hello')
})
it('should handle /index routes correctly', async () => {
const appHtml = await next.render('/index')
expect(appHtml).toContain('the /index route')
})
if ((globalThis as any).isNextDev) {
it('should warn about the re-export of a pages runtime/preferredRegion config', async () => {
const logs = []
next.on('stderr', (log) => {
logs.push(log)
})
const appHtml = await next.render('/export/inherit')
expect(appHtml).toContain('<p>Node!</p>')
expect(
logs.some((log) =>
log.includes(
`Next.js can't recognize the exported \`runtime\` field in`
)
)
).toBe(true)
expect(
logs.some((log) =>
log.includes(
`Next.js can't recognize the exported \`preferredRegion\` field in`
)
)
).toBe(true)
})
it('should resolve module without error in edge runtime', async () => {
const logs = []
next.on('stderr', (log) => {
logs.push(log)
})
await next.render('/app-edge')
expect(
logs.some((log) => log.includes(`Attempted import error:`))
).toBe(false)
})
it('should resolve client component without error', async () => {
const logs = []
next.on('stderr', (log) => {
logs.push(log)
})
const html = await next.render('/with-client')
expect(html).toContain('My Button')
expect(logs).toEqual([])
})
it('should handle edge rsc hmr', async () => {
const pageFile = 'app/edge/basic/page.tsx'
const content = await next.readFile(pageFile)
// Update rendered content
const updatedContent = content.replace('Edge!', 'edge-hmr')
await next.patchFile(pageFile, updatedContent)
await check(async () => {
const html = await next.render('/edge/basic')
return html
}, /edge-hmr/)
// Revert
await next.patchFile(pageFile, content)
await check(async () => {
const html = await next.render('/edge/basic')
return html
}, /Edge!/)
})
} else {
// Production tests
it('should generate matchers correctly in middleware manifest', async () => {
const manifest = JSON.parse(
await next.readFile('.next/server/middleware-manifest.json')
)
expect(manifest.functions['/(group)/group/page'].matchers).toEqual([
{
regexp: '^/group$',
originalSource: '/group',
},
])
})
}
if (skipped) {
return
}
)
it('should handle edge only routes', async () => {
const appHtml = await next.render('/edge/basic')
expect(appHtml).toContain('<p>Edge!</p>')
const pageHtml = await next.render('/pages-edge')
expect(pageHtml).toContain('<p>pages-edge-ssr</p>')
})
it('should retrieve cookies in a server component in the edge runtime', async () => {
const res = await next.fetch('/edge-apis/cookies')
expect(await res.text()).toInclude('Hello')
})
it('should handle /index routes correctly', async () => {
const appHtml = await next.render('/index')
expect(appHtml).toContain('the /index route')
})
if ((globalThis as any).isNextDev) {
it('should warn about the re-export of a pages runtime/preferredRegion config', async () => {
const logs = []
next.on('stderr', (log) => {
logs.push(log)
})
const appHtml = await next.render('/export/inherit')
expect(appHtml).toContain('<p>Node!</p>')
expect(
logs.some((log) =>
log.includes(
`Next.js can't recognize the exported \`runtime\` field in`
)
)
).toBe(true)
expect(
logs.some((log) =>
log.includes(
`Next.js can't recognize the exported \`preferredRegion\` field in`
)
)
).toBe(true)
})
it('should resolve module without error in edge runtime', async () => {
const logs = []
next.on('stderr', (log) => {
logs.push(log)
})
await next.render('/app-edge')
expect(logs.some((log) => log.includes(`Attempted import error:`))).toBe(
false
)
})
it('should resolve client component without error', async () => {
const logs = []
next.on('stderr', (log) => {
logs.push(log)
})
const html = await next.render('/with-client')
expect(html).toContain('My Button')
expect(logs).toEqual([])
})
it('should handle edge rsc hmr', async () => {
const pageFile = 'app/edge/basic/page.tsx'
const content = await next.readFile(pageFile)
// Update rendered content
const updatedContent = content.replace('Edge!', 'edge-hmr')
await next.patchFile(pageFile, updatedContent)
await check(async () => {
const html = await next.render('/edge/basic')
return html
}, /edge-hmr/)
// Revert
await next.patchFile(pageFile, content)
await check(async () => {
const html = await next.render('/edge/basic')
return html
}, /Edge!/)
})
} else {
// Production tests
it('should generate matchers correctly in middleware manifest', async () => {
const manifest = JSON.parse(
await next.readFile('.next/server/middleware-manifest.json')
)
expect(manifest.functions['/(group)/group/page'].matchers).toEqual([
{
regexp: '^/group$',
originalSource: '/group',
},
])
})
}
})

View file

@ -1,45 +1,43 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app-dir - esm js extension',
{
describe('app-dir - esm js extension', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should be able to render nextjs api in app router', async () => {
const $ = await next.render$('/app')
})
async function validateDomNodes(selector: string) {
expect(await $(`${selector} .img`).prop('tagName')).toBe('IMG')
expect(await $(`${selector} .link`).prop('tagName')).toBe('A')
expect(await $(`${selector} .typeof-getImageProps`).text()).toContain(
'function'
)
}
it('should be able to render nextjs api in app router', async () => {
const $ = await next.render$('/app')
await validateDomNodes('#with-ext')
await validateDomNodes('#without-ext')
expect($('head link[href="/test-ext.js"]').length).toBe(1)
expect($('head link[href="/test.js"]').length).toBe(1)
})
it('should be able to use nextjs api in pages router', async () => {
const $ = await next.render$('/pages')
expect(await $('meta[name="head-value-1"]').attr('content')).toBe(
'with-ext'
async function validateDomNodes(selector: string) {
expect(await $(`${selector} .img`).prop('tagName')).toBe('IMG')
expect(await $(`${selector} .link`).prop('tagName')).toBe('A')
expect(await $(`${selector} .typeof-getImageProps`).text()).toContain(
'function'
)
expect(await $('meta[name="head-value-2"]').attr('content')).toBe(
'without-ext'
)
expect(await $('.root').text()).toContain('pages')
})
}
it('should support next/og image', async () => {
const res = await next.fetch('/opengraph-image')
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toBe('image/png')
})
}
)
await validateDomNodes('#with-ext')
await validateDomNodes('#without-ext')
expect($('head link[href="/test-ext.js"]').length).toBe(1)
expect($('head link[href="/test.js"]').length).toBe(1)
})
it('should be able to use nextjs api in pages router', async () => {
const $ = await next.render$('/pages')
expect(await $('meta[name="head-value-1"]').attr('content')).toBe(
'with-ext'
)
expect(await $('meta[name="head-value-2"]').attr('content')).toBe(
'without-ext'
)
expect(await $('.root').text()).toContain('pages')
})
it('should support next/og image', async () => {
const res = await next.fetch('/opengraph-image')
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toBe('image/png')
})
})

View file

@ -1,4 +1,4 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check, hasRedbox, retry, shouldRunTurboDevTest } from 'next-test-utils'
async function resolveStreamResponse(response: any, onData?: any) {
@ -15,9 +15,8 @@ async function resolveStreamResponse(response: any, onData?: any) {
return result
}
createNextDescribe(
'app dir - external dependency',
{
describe('app dir - external dependency', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
dependencies: {
swr: 'latest',
@ -34,288 +33,283 @@ createNextDescribe(
startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start',
buildCommand: 'pnpm build',
skipDeployment: true,
},
({ next }) => {
it('should be able to opt-out 3rd party packages being bundled in server components', async () => {
await next.fetch('/react-server/optout').then(async (response) => {
const result = await resolveStreamResponse(response)
expect(result).toContain('Server: index.default')
expect(result).toContain('Server subpath: subpath.default')
expect(result).toContain('Client: index.default')
expect(result).toContain('Client subpath: subpath.default')
expect(result).toContain('opt-out-react-version: 18.2.0')
})
})
})
it('should handle external async module libraries correctly', async () => {
const clientHtml = await next.render('/external-imports/client')
const serverHtml = await next.render('/external-imports/server')
const sharedHtml = await next.render('/shared-esm-dep')
if (skipped) {
return
}
const browser = await next.browser('/external-imports/client')
const browserClientText = await browser.elementByCss('#content').text()
function containClientContent(content) {
expect(content).toContain('module type:esm-export')
expect(content).toContain('export named:named')
expect(content).toContain('export value:123')
expect(content).toContain('export array:4,5,6')
expect(content).toContain('export object:{x:1}')
expect(content).toContain('swr-state')
}
containClientContent(clientHtml)
containClientContent(browserClientText)
// support esm module imports on server side, and indirect imports from shared components
expect(serverHtml).toContain('pure-esm-module')
expect(sharedHtml).toContain(
'node_modules instance from client module pure-esm-module'
)
})
it('should transpile specific external packages with the `transpilePackages` option', async () => {
const clientHtml = await next.render('/external-imports/client')
expect(clientHtml).toContain('transpilePackages:5')
})
it('should resolve the subset react in server components based on the react-server condition', async () => {
await next.fetch('/react-server').then(async (response) => {
const result = await resolveStreamResponse(response)
expect(result).toContain('Server: <!-- -->subset')
expect(result).toContain('Client: <!-- -->full')
})
})
it('should resolve 3rd party package exports based on the react-server condition', async () => {
const $ = await next.render$('/react-server/3rd-party-package')
const result = $('body').text()
// Package should be resolved based on the react-server condition,
// as well as package's internal & external dependencies.
expect(result).toContain(
'Server: index.react-server:react.subset:dep.server'
)
expect(result).toContain('Client: index.default:react.full:dep.default')
// Subpath exports should be resolved based on the condition too.
expect(result).toContain('Server subpath: subpath.react-server')
it('should be able to opt-out 3rd party packages being bundled in server components', async () => {
await next.fetch('/react-server/optout').then(async (response) => {
const result = await resolveStreamResponse(response)
expect(result).toContain('Server: index.default')
expect(result).toContain('Server subpath: subpath.default')
expect(result).toContain('Client: index.default')
expect(result).toContain('Client subpath: subpath.default')
// Prefer `module` field for isomorphic packages.
expect($('#main-field').text()).toContain('server-module-field:module')
expect(result).toContain('opt-out-react-version: 18.2.0')
})
})
it('should correctly collect global css imports and mark them as side effects', async () => {
await next.fetch('/css/a').then(async (response) => {
const result = await resolveStreamResponse(response)
it('should handle external async module libraries correctly', async () => {
const clientHtml = await next.render('/external-imports/client')
const serverHtml = await next.render('/external-imports/server')
const sharedHtml = await next.render('/shared-esm-dep')
// It should include the global CSS import
expect(result).toMatch(/\.css/)
})
const browser = await next.browser('/external-imports/client')
const browserClientText = await browser.elementByCss('#content').text()
function containClientContent(content) {
expect(content).toContain('module type:esm-export')
expect(content).toContain('export named:named')
expect(content).toContain('export value:123')
expect(content).toContain('export array:4,5,6')
expect(content).toContain('export object:{x:1}')
expect(content).toContain('swr-state')
}
containClientContent(clientHtml)
containClientContent(browserClientText)
// support esm module imports on server side, and indirect imports from shared components
expect(serverHtml).toContain('pure-esm-module')
expect(sharedHtml).toContain(
'node_modules instance from client module pure-esm-module'
)
})
it('should transpile specific external packages with the `transpilePackages` option', async () => {
const clientHtml = await next.render('/external-imports/client')
expect(clientHtml).toContain('transpilePackages:5')
})
it('should resolve the subset react in server components based on the react-server condition', async () => {
await next.fetch('/react-server').then(async (response) => {
const result = await resolveStreamResponse(response)
expect(result).toContain('Server: <!-- -->subset')
expect(result).toContain('Client: <!-- -->full')
})
})
it('should handle external css modules', async () => {
const browser = await next.browser('/css/modules')
it('should resolve 3rd party package exports based on the react-server condition', async () => {
const $ = await next.render$('/react-server/3rd-party-package')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
)
).toBe('rgb(255, 0, 0)')
const result = $('body').text()
// Package should be resolved based on the react-server condition,
// as well as package's internal & external dependencies.
expect(result).toContain(
'Server: index.react-server:react.subset:dep.server'
)
expect(result).toContain('Client: index.default:react.full:dep.default')
// Subpath exports should be resolved based on the condition too.
expect(result).toContain('Server subpath: subpath.react-server')
expect(result).toContain('Client subpath: subpath.default')
// Prefer `module` field for isomorphic packages.
expect($('#main-field').text()).toContain('server-module-field:module')
})
it('should correctly collect global css imports and mark them as side effects', async () => {
await next.fetch('/css/a').then(async (response) => {
const result = await resolveStreamResponse(response)
// It should include the global CSS import
expect(result).toMatch(/\.css/)
})
})
it('should use the same export type for packages in both ssr and client', async () => {
const browser = await next.browser('/client-dep')
expect(await browser.eval(`window.document.body.innerText`)).toBe('hello')
})
it('should handle external css modules', async () => {
const browser = await next.browser('/css/modules')
it('should handle external css modules in pages', async () => {
const browser = await next.browser('/test-pages')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
)
).toBe('rgb(255, 0, 0)')
})
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
)
).toBe('rgb(255, 0, 0)')
})
it('should use the same export type for packages in both ssr and client', async () => {
const browser = await next.browser('/client-dep')
expect(await browser.eval(`window.document.body.innerText`)).toBe('hello')
})
it('should handle external next/font', async () => {
const browser = await next.browser('/font')
it('should handle external css modules in pages', async () => {
const browser = await next.browser('/test-pages')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('p')).fontFamily`
)
).toMatch(/^__myFont_.{6}, __myFont_Fallback_.{6}$/)
})
// TODO: This test depends on `new Worker` which is not supported in Turbopack yet.
;(process.env.TURBOPACK ? it.skip : it)(
'should not apply swc optimizer transform for external packages in browser layer in web worker',
async () => {
const browser = await next.browser('/browser')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
)
).toBe('rgb(255, 0, 0)')
})
it('should handle external next/font', async () => {
const browser = await next.browser('/font')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('p')).fontFamily`
)
).toMatch(/^__myFont_.{6}, __myFont_Fallback_.{6}$/)
})
// TODO: This test depends on `new Worker` which is not supported in Turbopack yet.
;(process.env.TURBOPACK ? it.skip : it)(
'should not apply swc optimizer transform for external packages in browser layer in web worker',
async () => {
const browser = await next.browser('/browser')
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('#worker-state').text()).toBe('default')
await browser.elementByCss('button').click()
await retry(async () => {
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('#worker-state').text()).toBe(
'default'
)
await browser.elementByCss('button').click()
await retry(async () => {
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('#worker-state').text()).toBe(
'worker.js:browser-module/other'
)
})
}
)
describe('react in external esm packages', () => {
it('should use the same react in client app', async () => {
const html = await next.render('/esm/client')
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
// Should work with both esm and cjs imports
expect(html).toContain(
'CJS-ESM Compat package: cjs-esm-compat/index.mjs'
)
expect(html).toContain('CJS package: cjs-lib')
expect(html).toContain(
'Nested imports: nested-import:esm:cjs-esm-compat/index.mjs'
'worker.js:browser-module/other'
)
})
}
)
it('should use the same react in server app', async () => {
const html = await next.render('/esm/server')
describe('react in external esm packages', () => {
it('should use the same react in client app', async () => {
const html = await next.render('/esm/client')
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
// Should work with both esm and cjs imports
expect(html).toContain(
'CJS-ESM Compat package: cjs-esm-compat/index.mjs'
)
expect(html).toContain('CJS package: cjs-lib')
})
it('should use the same react in edge server app', async () => {
const html = await next.render('/esm/edge-server')
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
// Should work with both esm and cjs imports
expect(html).toContain(
'CJS-ESM Compat package: cjs-esm-compat/index.mjs'
)
expect(html).toContain('CJS package: cjs-lib')
})
it('should use the same react in pages', async () => {
const html = await next.render('/test-pages-esm')
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
})
it('should support namespace import with ESM packages', async () => {
const $ = await next.render$('/esm/react-namespace-import')
expect($('#namespace-import-esm').text()).toBe('namespace-import:esm')
})
})
describe('mixed syntax external modules', () => {
it('should handle mixed module with next/dynamic', async () => {
const browser = await next.browser('/mixed/dynamic')
expect(await browser.elementByCss('#component').text()).toContain(
'mixed-syntax-esm'
)
})
it('should handle mixed module in server and client components', async () => {
const $ = await next.render$('/mixed/import')
expect(await $('#server').text()).toContain('server:mixed-syntax-esm')
expect(await $('#client').text()).toContain('client:mixed-syntax-esm')
expect(await $('#relative-mixed').text()).toContain(
'relative-mixed-syntax-esm'
)
})
})
it('should emit cjs helpers for external cjs modules when compiled', async () => {
const $ = await next.render$('/cjs/client')
expect($('#private-prop').text()).toBe('prop')
expect($('#transpile-cjs-lib').text()).toBe('transpile-cjs-lib')
const browser = await next.browser('/cjs/client')
expect(await hasRedbox(browser)).toBe(false)
})
it('should export client module references in esm', async () => {
const html = await next.render('/esm-client-ref')
expect(html).toContain('hello')
})
it('should support exporting multiple star re-exports', async () => {
const html = await next.render('/wildcard')
expect(html).toContain('Foo')
})
it('should have proper tree-shaking for known modules in CJS', async () => {
const html = await next.render('/cjs/server')
expect(html).toContain('resolve response')
const outputFile = await next.readFile(
'.next/server/app/cjs/server/page.js'
// Should work with both esm and cjs imports
expect(html).toContain('CJS-ESM Compat package: cjs-esm-compat/index.mjs')
expect(html).toContain('CJS package: cjs-lib')
expect(html).toContain(
'Nested imports: nested-import:esm:cjs-esm-compat/index.mjs'
)
expect(outputFile).not.toContain('image-response')
})
it('should use the same async storages if imported directly', async () => {
const html = await next.render('/async-storage')
expect(html).toContain('success')
it('should use the same react in server app', async () => {
const html = await next.render('/esm/server')
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
// Should work with both esm and cjs imports
expect(html).toContain('CJS-ESM Compat package: cjs-esm-compat/index.mjs')
expect(html).toContain('CJS package: cjs-lib')
})
describe('server actions', () => {
it('should not prefer to resolve esm over cjs for bundling optout packages', async () => {
const browser = await next.browser('/optout/action')
expect(await browser.elementByCss('#dual-pkg-outout p').text()).toBe('')
it('should use the same react in edge server app', async () => {
const html = await next.render('/esm/edge-server')
browser.elementByCss('#dual-pkg-outout button').click()
await check(async () => {
const text = await browser.elementByCss('#dual-pkg-outout p').text()
expect(text).toBe('dual-pkg-optout:cjs')
return 'success'
}, /success/)
})
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
it('should compile server actions from node_modules in client components', async () => {
// before action there's no action log
expect(next.cliOutput).not.toContain('action-log:server:action1')
const browser = await next.browser('/action/client')
await browser.elementByCss('#action').click()
await check(() => {
expect(next.cliOutput).toContain('action-log:server:action1')
return 'success'
}, /success/)
})
// Should work with both esm and cjs imports
expect(html).toContain('CJS-ESM Compat package: cjs-esm-compat/index.mjs')
expect(html).toContain('CJS package: cjs-lib')
})
describe('app route', () => {
it('should resolve next/server api from external esm package', async () => {
const res = await next.fetch('/app-routes')
const text = await res.text()
expect(res.status).toBe(200)
expect(text).toBe('get route')
})
it('should use the same react in pages', async () => {
const html = await next.render('/test-pages-esm')
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
})
}
)
it('should support namespace import with ESM packages', async () => {
const $ = await next.render$('/esm/react-namespace-import')
expect($('#namespace-import-esm').text()).toBe('namespace-import:esm')
})
})
describe('mixed syntax external modules', () => {
it('should handle mixed module with next/dynamic', async () => {
const browser = await next.browser('/mixed/dynamic')
expect(await browser.elementByCss('#component').text()).toContain(
'mixed-syntax-esm'
)
})
it('should handle mixed module in server and client components', async () => {
const $ = await next.render$('/mixed/import')
expect(await $('#server').text()).toContain('server:mixed-syntax-esm')
expect(await $('#client').text()).toContain('client:mixed-syntax-esm')
expect(await $('#relative-mixed').text()).toContain(
'relative-mixed-syntax-esm'
)
})
})
it('should emit cjs helpers for external cjs modules when compiled', async () => {
const $ = await next.render$('/cjs/client')
expect($('#private-prop').text()).toBe('prop')
expect($('#transpile-cjs-lib').text()).toBe('transpile-cjs-lib')
const browser = await next.browser('/cjs/client')
expect(await hasRedbox(browser)).toBe(false)
})
it('should export client module references in esm', async () => {
const html = await next.render('/esm-client-ref')
expect(html).toContain('hello')
})
it('should support exporting multiple star re-exports', async () => {
const html = await next.render('/wildcard')
expect(html).toContain('Foo')
})
it('should have proper tree-shaking for known modules in CJS', async () => {
const html = await next.render('/cjs/server')
expect(html).toContain('resolve response')
const outputFile = await next.readFile(
'.next/server/app/cjs/server/page.js'
)
expect(outputFile).not.toContain('image-response')
})
it('should use the same async storages if imported directly', async () => {
const html = await next.render('/async-storage')
expect(html).toContain('success')
})
describe('server actions', () => {
it('should not prefer to resolve esm over cjs for bundling optout packages', async () => {
const browser = await next.browser('/optout/action')
expect(await browser.elementByCss('#dual-pkg-outout p').text()).toBe('')
browser.elementByCss('#dual-pkg-outout button').click()
await check(async () => {
const text = await browser.elementByCss('#dual-pkg-outout p').text()
expect(text).toBe('dual-pkg-optout:cjs')
return 'success'
}, /success/)
})
it('should compile server actions from node_modules in client components', async () => {
// before action there's no action log
expect(next.cliOutput).not.toContain('action-log:server:action1')
const browser = await next.browser('/action/client')
await browser.elementByCss('#action').click()
await check(() => {
expect(next.cliOutput).toContain('action-log:server:action1')
return 'success'
}, /success/)
})
})
describe('app route', () => {
it('should resolve next/server api from external esm package', async () => {
const res = await next.fetch('/app-routes')
const text = await res.text()
expect(res.status).toBe(200)
expect(text).toBe('get route')
})
})
})

View file

@ -1,100 +1,102 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'app-invalid-revalidate',
{
describe('app-invalid-revalidate', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
skipDeployment: true,
},
({ next, isNextDev }) => {
it('should error properly for invalid revalidate at layout', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/layout.tsx')
})
try {
await next.patchFile(
'app/layout.tsx',
origText.replace('// export', 'export')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "\/", must be a non-negative number or "false"/)
} finally {
await next.patchFile('app/layout.tsx', origText)
}
})
it('should error properly for invalid revalidate at page', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/page.tsx')
try {
await next.patchFile(
'app/page.tsx',
origText.replace('// export', 'export')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "\/", must be a non-negative number or "false"/)
} finally {
await next.patchFile('app/page.tsx', origText)
}
})
it('should error properly for invalid revalidate on fetch', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/page.tsx')
try {
await next.patchFile(
'app/page.tsx',
origText.replace('// await', 'await')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "\/", must be a non-negative number or "false"/)
} finally {
await next.patchFile('app/page.tsx', origText)
}
})
it('should error properly for invalid revalidate on unstable_cache', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/page.tsx')
try {
await next.patchFile(
'app/page.tsx',
origText.replace('// await unstable', 'await unstable')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "unstable_cache/)
} finally {
await next.patchFile('app/page.tsx', origText)
}
})
if (skipped) {
return
}
)
it('should error properly for invalid revalidate at layout', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/layout.tsx')
try {
await next.patchFile(
'app/layout.tsx',
origText.replace('// export', 'export')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "\/", must be a non-negative number or "false"/)
} finally {
await next.patchFile('app/layout.tsx', origText)
}
})
it('should error properly for invalid revalidate at page', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/page.tsx')
try {
await next.patchFile(
'app/page.tsx',
origText.replace('// export', 'export')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "\/", must be a non-negative number or "false"/)
} finally {
await next.patchFile('app/page.tsx', origText)
}
})
it('should error properly for invalid revalidate on fetch', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/page.tsx')
try {
await next.patchFile(
'app/page.tsx',
origText.replace('// await', 'await')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "\/", must be a non-negative number or "false"/)
} finally {
await next.patchFile('app/page.tsx', origText)
}
})
it('should error properly for invalid revalidate on unstable_cache', async () => {
await next.stop().catch(() => {})
const origText = await next.readFile('app/page.tsx')
try {
await next.patchFile(
'app/page.tsx',
origText.replace('// await unstable', 'await unstable')
)
await next.start().catch(() => {})
await check(async () => {
if (isNextDev) {
await next.fetch('/')
}
return next.cliOutput
}, /Invalid revalidate value "1" on "unstable_cache/)
} finally {
await next.patchFile('app/page.tsx', origText)
}
})
})

View file

@ -2,179 +2,168 @@
import path from 'path'
import cheerio from 'cheerio'
import { check, retry, withQuery } from 'next-test-utils'
import { createNextDescribe, FileRef } from 'e2e-utils'
import { nextTestSetup, FileRef } from 'e2e-utils'
import type { Response } from 'node-fetch'
createNextDescribe(
'app-dir with middleware',
{
describe('app-dir with middleware', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('should filter correctly after middleware rewrite', async () => {
const browser = await next.browser('/start')
})
await browser.eval('window.beforeNav = 1')
await browser.eval('window.next.router.push("/rewrite-to-app")')
await check(async () => {
return browser.eval('document.documentElement.innerHTML')
}, /app-dir/)
})
describe.each([
{
title: 'Serverless Functions',
path: '/api/dump-headers-serverless',
toJson: (res: Response) => res.json(),
},
{
title: 'Edge Functions',
path: '/api/dump-headers-edge',
toJson: (res: Response) => res.json(),
},
{
title: 'next/headers',
path: '/headers',
toJson: async (res: Response) => {
const $ = cheerio.load(await res.text())
return JSON.parse($('#headers').text())
},
},
])('Mutate request headers for $title', ({ path, toJson }) => {
it(`Adds new headers`, async () => {
const res = await next.fetch(path, {
headers: {
'x-from-client': 'hello-from-client',
},
})
expect(await toJson(res)).toMatchObject({
'x-from-client': 'hello-from-client',
'x-from-middleware': 'hello-from-middleware',
})
})
it(`Deletes headers`, async () => {
const res = await next.fetch(
withQuery(path, {
'remove-headers': 'x-from-client1,x-from-client2',
}),
{
headers: {
'x-from-client1': 'hello-from-client',
'X-From-Client2': 'hello-from-client',
},
}
)
const json = await toJson(res)
expect(json).not.toHaveProperty('x-from-client1')
expect(json).not.toHaveProperty('X-From-Client2')
expect(json).toMatchObject({
'x-from-middleware': 'hello-from-middleware',
})
// Should not be included in response headers.
expect(res.headers.get('x-middleware-override-headers')).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-middleware')
).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-client1')
).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-client2')
).toBeNull()
})
it(`Updates headers`, async () => {
const res = await next.fetch(
withQuery(path, {
'update-headers':
'x-from-client1=new-value1,x-from-client2=new-value2',
}),
{
headers: {
'x-from-client1': 'old-value1',
'X-From-Client2': 'old-value2',
'x-from-client3': 'old-value3',
},
}
)
expect(await toJson(res)).toMatchObject({
'x-from-client1': 'new-value1',
'x-from-client2': 'new-value2',
'x-from-client3': 'old-value3',
'x-from-middleware': 'hello-from-middleware',
})
// Should not be included in response headers.
expect(res.headers.get('x-middleware-override-headers')).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-middleware')
).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-client1')
).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-client2')
).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-client3')
).toBeNull()
})
it(`Supports draft mode`, async () => {
const res = await next.fetch(`${path}?draft=true`)
const headers: string = res.headers.get('set-cookie') || ''
const bypassCookie = headers
.split(';')
.find((c) => c.startsWith('__prerender_bypass'))
expect(bypassCookie).toBeDefined()
})
})
it('should be possible to modify cookies & read them in an RSC in a single request', async () => {
const browser = await next.browser('/rsc-cookies')
const initialRandom1 = await browser.elementById('rsc-cookie-1').text()
const initialRandom2 = await browser.elementById('rsc-cookie-2').text()
// cookies were set in middleware, assert they are present and match the Math.random() pattern
expect(initialRandom1).toMatch(/Cookie 1: \d+\.\d+/)
expect(initialRandom2).toMatch(/Cookie 2: \d+\.\d+/)
await browser.refresh()
const refreshedRandom1 = await browser.elementById('rsc-cookie-1').text()
const refreshedRandom2 = await browser.elementById('rsc-cookie-2').text()
// the cookies should be refreshed and have new values
expect(refreshedRandom1).toMatch(/Cookie 1: \d+\.\d+/)
expect(refreshedRandom2).toMatch(/Cookie 2: \d+\.\d+/)
expect(refreshedRandom1).not.toBe(initialRandom1)
expect(refreshedRandom2).not.toBe(initialRandom2)
// navigate to delete cookies route
await browser.elementByCss('[href="/rsc-cookies-delete"]').click()
await retry(async () => {
// only the first cookie should be deleted
expect(await browser.elementById('rsc-cookie-1').text()).toBe(
'Cookie 1:'
)
expect(await browser.elementById('rsc-cookie-2').text()).toMatch(
/Cookie 2: \d+\.\d+/
)
})
})
if (skipped) {
return
}
)
createNextDescribe(
'app dir - middleware without pages dir',
{
it('should filter correctly after middleware rewrite', async () => {
const browser = await next.browser('/start')
await browser.eval('window.beforeNav = 1')
await browser.eval('window.next.router.push("/rewrite-to-app")')
await check(async () => {
return browser.eval('document.documentElement.innerHTML')
}, /app-dir/)
})
describe.each([
{
title: 'Serverless Functions',
path: '/api/dump-headers-serverless',
toJson: (res: Response) => res.json(),
},
{
title: 'Edge Functions',
path: '/api/dump-headers-edge',
toJson: (res: Response) => res.json(),
},
{
title: 'next/headers',
path: '/headers',
toJson: async (res: Response) => {
const $ = cheerio.load(await res.text())
return JSON.parse($('#headers').text())
},
},
])('Mutate request headers for $title', ({ path, toJson }) => {
it(`Adds new headers`, async () => {
const res = await next.fetch(path, {
headers: {
'x-from-client': 'hello-from-client',
},
})
expect(await toJson(res)).toMatchObject({
'x-from-client': 'hello-from-client',
'x-from-middleware': 'hello-from-middleware',
})
})
it(`Deletes headers`, async () => {
const res = await next.fetch(
withQuery(path, {
'remove-headers': 'x-from-client1,x-from-client2',
}),
{
headers: {
'x-from-client1': 'hello-from-client',
'X-From-Client2': 'hello-from-client',
},
}
)
const json = await toJson(res)
expect(json).not.toHaveProperty('x-from-client1')
expect(json).not.toHaveProperty('X-From-Client2')
expect(json).toMatchObject({
'x-from-middleware': 'hello-from-middleware',
})
// Should not be included in response headers.
expect(res.headers.get('x-middleware-override-headers')).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-middleware')
).toBeNull()
expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull()
expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull()
})
it(`Updates headers`, async () => {
const res = await next.fetch(
withQuery(path, {
'update-headers':
'x-from-client1=new-value1,x-from-client2=new-value2',
}),
{
headers: {
'x-from-client1': 'old-value1',
'X-From-Client2': 'old-value2',
'x-from-client3': 'old-value3',
},
}
)
expect(await toJson(res)).toMatchObject({
'x-from-client1': 'new-value1',
'x-from-client2': 'new-value2',
'x-from-client3': 'old-value3',
'x-from-middleware': 'hello-from-middleware',
})
// Should not be included in response headers.
expect(res.headers.get('x-middleware-override-headers')).toBeNull()
expect(
res.headers.get('x-middleware-request-x-from-middleware')
).toBeNull()
expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull()
expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull()
expect(res.headers.get('x-middleware-request-x-from-client3')).toBeNull()
})
it(`Supports draft mode`, async () => {
const res = await next.fetch(`${path}?draft=true`)
const headers: string = res.headers.get('set-cookie') || ''
const bypassCookie = headers
.split(';')
.find((c) => c.startsWith('__prerender_bypass'))
expect(bypassCookie).toBeDefined()
})
})
it('should be possible to modify cookies & read them in an RSC in a single request', async () => {
const browser = await next.browser('/rsc-cookies')
const initialRandom1 = await browser.elementById('rsc-cookie-1').text()
const initialRandom2 = await browser.elementById('rsc-cookie-2').text()
// cookies were set in middleware, assert they are present and match the Math.random() pattern
expect(initialRandom1).toMatch(/Cookie 1: \d+\.\d+/)
expect(initialRandom2).toMatch(/Cookie 2: \d+\.\d+/)
await browser.refresh()
const refreshedRandom1 = await browser.elementById('rsc-cookie-1').text()
const refreshedRandom2 = await browser.elementById('rsc-cookie-2').text()
// the cookies should be refreshed and have new values
expect(refreshedRandom1).toMatch(/Cookie 1: \d+\.\d+/)
expect(refreshedRandom2).toMatch(/Cookie 2: \d+\.\d+/)
expect(refreshedRandom1).not.toBe(initialRandom1)
expect(refreshedRandom2).not.toBe(initialRandom2)
// navigate to delete cookies route
await browser.elementByCss('[href="/rsc-cookies-delete"]').click()
await retry(async () => {
// only the first cookie should be deleted
expect(await browser.elementById('rsc-cookie-1').text()).toBe('Cookie 1:')
expect(await browser.elementById('rsc-cookie-2').text()).toMatch(
/Cookie 2: \d+\.\d+/
)
})
})
})
describe('app dir - middleware without pages dir', () => {
const { next, skipped } = nextTestSetup({
files: {
app: new FileRef(path.join(__dirname, 'app')),
'next.config.js': new FileRef(path.join(__dirname, 'next.config.js')),
@ -191,20 +180,22 @@ createNextDescribe(
`,
},
skipDeployment: true,
},
({ next }) => {
// eslint-disable-next-line jest/no-identical-title
it('Updates headers', async () => {
const html = await next.render('/headers')
})
expect(html).toContain('redirected')
})
if (skipped) {
return
}
)
createNextDescribe(
'app dir - middleware with middleware in src dir',
{
// eslint-disable-next-line jest/no-identical-title
it('Updates headers', async () => {
const html = await next.render('/headers')
expect(html).toContain('redirected')
})
})
describe('app dir - middleware with middleware in src dir', () => {
const { next, skipped } = nextTestSetup({
files: {
'src/app': new FileRef(path.join(__dirname, 'app')),
'next.config.js': new FileRef(path.join(__dirname, 'next.config.js')),
@ -219,19 +210,22 @@ createNextDescribe(
`,
},
skipDeployment: true,
},
({ next }) => {
it('works without crashing when using requestAsyncStorage', async () => {
const browser = await next.browser('/')
await browser.addCookie({
name: 'test-cookie',
value: 'test-cookie-response',
})
await browser.refresh()
})
const html = await browser.eval('document.documentElement.innerHTML')
expect(html).toContain('test-cookie-response')
})
if (skipped) {
return
}
)
it('works without crashing when using requestAsyncStorage', async () => {
const browser = await next.browser('/')
await browser.addCookie({
name: 'test-cookie',
value: 'test-cookie-response',
})
await browser.refresh()
const html = await browser.eval('document.documentElement.innerHTML')
expect(html).toContain('test-cookie-response')
})
})

View file

@ -1,43 +1,39 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'app-prefetch-false-loading',
{
describe('app-prefetch-false-loading', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should render loading for the initial render', async () => {
const $ = await next.render$('/en/testing')
})
expect($('#loading').text()).toBe('Loading...')
})
it('should not re-trigger loading state when navigating between pages that share a dynamic layout', async () => {
const logStartIndex = next.cliOutput.length
it('should render loading for the initial render', async () => {
const $ = await next.render$('/en/testing')
const browser = await next.browser('/en/testing')
let initialRandomNumber = await browser
.elementById('random-number')
.text()
await browser.elementByCss('[href="/en/testing/test"]').click()
expect(await browser.hasElementByCssSelector('#loading')).toBeFalsy()
expect($('#loading').text()).toBe('Loading...')
})
it('should not re-trigger loading state when navigating between pages that share a dynamic layout', async () => {
const logStartIndex = next.cliOutput.length
await check(
() => browser.hasElementByCssSelector('#nested-testing-page'),
true
)
const browser = await next.browser('/en/testing')
let initialRandomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/en/testing/test"]').click()
expect(await browser.hasElementByCssSelector('#loading')).toBeFalsy()
const newRandomNumber = await browser.elementById('random-number').text()
await check(
() => browser.hasElementByCssSelector('#nested-testing-page'),
true
)
expect(initialRandomNumber).toBe(newRandomNumber)
const newRandomNumber = await browser.elementById('random-number').text()
await check(() => {
const logOccurrences =
next.cliOutput.slice(logStartIndex).split('re-fetching in layout')
.length - 1
expect(initialRandomNumber).toBe(newRandomNumber)
return logOccurrences
}, 1)
})
}
)
await check(() => {
const logOccurrences =
next.cliOutput.slice(logStartIndex).split('re-fetching in layout')
.length - 1
return logOccurrences
}, 1)
})
})

View file

@ -1,6 +1,6 @@
import type { Request } from 'playwright'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import type { BrowserInterface } from '../../../lib/browsers/base'
const getPathname = (url: string) => {
@ -28,27 +28,24 @@ const createRequestsListener = async (browser: BrowserInterface) => {
}
}
createNextDescribe(
'app-prefetch-false',
{
describe('app-prefetch-false', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
},
({ next, isNextDev }) => {
if (isNextDev) {
it.skip('should skip test in development mode', () => {})
} else {
it('should avoid double-fetching when optimistic navigation fails', async () => {
const browser = await next.browser('/foo')
const { getRequests } = await createRequestsListener(browser)
})
await browser.elementByCss('[href="/foo"]').click()
await browser.elementByCss('[href="/foo/bar"]').click()
console.log('getRequests()', getRequests())
expect(
getRequests().filter(([req]) => getPathname(req) === '/foo/bar')
.length
).toBe(1)
})
}
if (isNextDev) {
it.skip('should skip test in development mode', () => {})
} else {
it('should avoid double-fetching when optimistic navigation fails', async () => {
const browser = await next.browser('/foo')
const { getRequests } = await createRequestsListener(browser)
await browser.elementByCss('[href="/foo"]').click()
await browser.elementByCss('[href="/foo/bar"]').click()
console.log('getRequests()', getRequests())
expect(
getRequests().filter(([req]) => getPathname(req) === '/foo/bar').length
).toBe(1)
})
}
)
})

View file

@ -1,39 +1,37 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { waitFor } from 'next-test-utils'
createNextDescribe(
'app-prefetch-static',
{
describe('app-prefetch-static', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
},
({ next, isNextDev }) => {
if (isNextDev) {
it('should skip next dev', () => {})
return
}
})
it('should correctly navigate between static & dynamic pages', async () => {
const browser = await next.browser('/')
// Ensure the page is prefetched
await waitFor(1000)
await browser.elementByCss('#static-prefetch').click()
expect(await browser.elementByCss('#static-prefetch-page').text()).toBe(
'Hello from Static Prefetch Page'
)
await browser.elementByCss('#dynamic-prefetch').click()
expect(await browser.elementByCss('#dynamic-prefetch-page').text()).toBe(
'Hello from Dynamic Prefetch Page'
)
await browser.elementByCss('#static-prefetch').click()
expect(await browser.elementByCss('#static-prefetch-page').text()).toBe(
'Hello from Static Prefetch Page'
)
})
if (isNextDev) {
it('should skip next dev', () => {})
return
}
)
it('should correctly navigate between static & dynamic pages', async () => {
const browser = await next.browser('/')
// Ensure the page is prefetched
await waitFor(1000)
await browser.elementByCss('#static-prefetch').click()
expect(await browser.elementByCss('#static-prefetch-page').text()).toBe(
'Hello from Static Prefetch Page'
)
await browser.elementByCss('#dynamic-prefetch').click()
expect(await browser.elementByCss('#dynamic-prefetch-page').text()).toBe(
'Hello from Dynamic Prefetch Page'
)
await browser.elementByCss('#static-prefetch').click()
expect(await browser.elementByCss('#static-prefetch-page').text()).toBe(
'Hello from Static Prefetch Page'
)
})
})

View file

@ -1,4 +1,4 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'
@ -27,389 +27,384 @@ const browserConfigWithFixedTime = {
},
}
createNextDescribe(
'app dir - prefetching',
{
describe('app dir - prefetching', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextDev }) => {
// TODO: re-enable for dev after https://vercel.slack.com/archives/C035J346QQL/p1663822388387959 is resolved (Sep 22nd 2022)
if (isNextDev) {
it('should skip next dev for now', () => {})
return
}
})
it('NEXT_RSC_UNION_QUERY query name is _rsc', async () => {
expect(NEXT_RSC_UNION_QUERY).toBe('_rsc')
if (skipped) {
return
}
// TODO: re-enable for dev after https://vercel.slack.com/archives/C035J346QQL/p1663822388387959 is resolved (Sep 22nd 2022)
if (isNextDev) {
it('should skip next dev for now', () => {})
return
}
it('NEXT_RSC_UNION_QUERY query name is _rsc', async () => {
expect(NEXT_RSC_UNION_QUERY).toBe('_rsc')
})
it('should show layout eagerly when prefetched with loading one level down', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Ensure the page is prefetched
await waitFor(1000)
const before = Date.now()
await browser
.elementByCss('#to-dashboard')
.click()
.waitForElementByCss('#dashboard-layout')
const after = Date.now()
const timeToComplete = after - before
expect(timeToComplete).toBeLessThan(1000)
expect(await browser.elementByCss('#dashboard-layout').text()).toBe(
'Dashboard Hello World'
)
await browser.waitForElementByCss('#dashboard-page')
expect(await browser.waitForElementByCss('#dashboard-page').text()).toBe(
'Welcome to the dashboard'
)
})
it('should not have prefetch error for static path', async () => {
const browser = await next.browser('/')
await browser.eval('window.nd.router.prefetch("/dashboard/123")')
await waitFor(3000)
await browser.eval('window.nd.router.push("/dashboard/123")')
expect(next.cliOutput).not.toContain('ReferenceError')
expect(next.cliOutput).not.toContain('is not defined')
})
it('should not fetch again when a static page was prefetched', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
let requests: string[] = []
browser.on('request', (req) => {
requests.push(new URL(req.url()).pathname)
})
await browser.eval('location.href = "/"')
it('should show layout eagerly when prefetched with loading one level down', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Ensure the page is prefetched
await waitFor(1000)
await browser.eval(
'window.nd.router.prefetch("/static-page", {kind: "auto"})'
)
const before = Date.now()
await browser
.elementByCss('#to-dashboard')
.click()
.waitForElementByCss('#dashboard-layout')
const after = Date.now()
const timeToComplete = after - before
expect(timeToComplete).toBeLessThan(1000)
expect(await browser.elementByCss('#dashboard-layout').text()).toBe(
'Dashboard Hello World'
await check(() => {
return requests.some(
(req) =>
req.includes('static-page') && !req.includes(NEXT_RSC_UNION_QUERY)
)
? 'success'
: JSON.stringify(requests)
}, 'success')
await browser.waitForElementByCss('#dashboard-page')
await browser
.elementByCss('#to-static-page')
.click()
.waitForElementByCss('#static-page')
expect(await browser.waitForElementByCss('#dashboard-page').text()).toBe(
'Welcome to the dashboard'
)
expect(
requests.filter((request) => request === '/static-page').length
).toBe(1)
})
it('should not fetch again when a static page was prefetched when navigating to it twice', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
let requests: string[] = []
browser.on('request', (req) => {
requests.push(new URL(req.url()).pathname)
})
await browser.eval('location.href = "/"')
it('should not have prefetch error for static path', async () => {
const browser = await next.browser('/')
await browser.eval('window.nd.router.prefetch("/dashboard/123")')
await waitFor(3000)
await browser.eval('window.nd.router.push("/dashboard/123")')
expect(next.cliOutput).not.toContain('ReferenceError')
expect(next.cliOutput).not.toContain('is not defined')
})
it('should not fetch again when a static page was prefetched', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
let requests: string[] = []
browser.on('request', (req) => {
requests.push(new URL(req.url()).pathname)
})
await browser.eval('location.href = "/"')
await browser.eval(
'window.nd.router.prefetch("/static-page", {kind: "auto"})'
await browser.eval(
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
)
await check(() => {
return requests.some(
(req) =>
req.includes('static-page') && !req.includes(NEXT_RSC_UNION_QUERY)
)
? 'success'
: JSON.stringify(requests)
}, 'success')
await check(() => {
return requests.some(
(req) =>
req.includes('static-page') && !req.includes(NEXT_RSC_UNION_QUERY)
)
? 'success'
: JSON.stringify(requests)
}, 'success')
await browser
.elementByCss('#to-static-page')
.click()
.waitForElementByCss('#static-page')
await browser
.elementByCss('#to-static-page')
.click()
.waitForElementByCss('#static-page')
await browser
.elementByCss('#to-home')
// Go back to home page
.click()
// Wait for homepage to load
.waitForElementByCss('#to-static-page')
// Click on the link to the static page again
.click()
// Wait for the static page to load again
.waitForElementByCss('#static-page')
expect(
requests.filter((request) => request === '/static-page').length
).toBe(1)
expect(
requests.filter(
(request) =>
request === '/static-page' || request.includes(NEXT_RSC_UNION_QUERY)
).length
).toBe(1)
})
it('should calculate `_rsc` query based on `Next-Url`', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
let staticPageRequests: string[] = []
browser.on('request', (req) => {
const url = new URL(req.url())
if (url.toString().includes(`/static-page?${NEXT_RSC_UNION_QUERY}=`)) {
staticPageRequests.push(`${url.pathname}${url.search}`)
}
})
await browser.eval('location.href = "/"')
await browser.eval(
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
)
await check(() => {
return staticPageRequests.length === 1
? 'success'
: JSON.stringify(staticPageRequests)
}, 'success')
it('should not fetch again when a static page was prefetched when navigating to it twice', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
let requests: string[] = []
// Unable to clear router cache so mpa navigation
await browser.eval('location.href = "/dashboard"')
await browser.eval(
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
)
await check(() => {
return staticPageRequests.length === 2
? 'success'
: JSON.stringify(staticPageRequests)
}, 'success')
browser.on('request', (req) => {
requests.push(new URL(req.url()).pathname)
})
await browser.eval('location.href = "/"')
expect(staticPageRequests[0]).toMatch('/static-page?_rsc=')
expect(staticPageRequests[1]).toMatch('/static-page?_rsc=')
// `_rsc` does not match because it depends on the `Next-Url`
expect(staticPageRequests[0]).not.toBe(staticPageRequests[1])
})
await browser.eval(
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
)
await check(() => {
return requests.some(
(req) =>
req.includes('static-page') && !req.includes(NEXT_RSC_UNION_QUERY)
)
? 'success'
: JSON.stringify(requests)
}, 'success')
it('should not prefetch for a bot user agent', async () => {
const browser = await next.browser('/404')
let requests: string[] = []
await browser
.elementByCss('#to-static-page')
.click()
.waitForElementByCss('#static-page')
browser.on('request', (req) => {
requests.push(new URL(req.url()).pathname)
})
await browser.eval(
`location.href = "/?useragent=${encodeURIComponent(
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
)}"`
)
await browser
.elementByCss('#to-home')
// Go back to home page
.click()
// Wait for homepage to load
.waitForElementByCss('#to-static-page')
// Click on the link to the static page again
.click()
// Wait for the static page to load again
.waitForElementByCss('#static-page')
await browser.elementByCss('#to-static-page').moveTo()
// check five times to ensure prefetch didn't occur
for (let i = 0; i < 5; i++) {
await waitFor(500)
expect(
requests.filter(
(request) =>
request === '/static-page' || request.includes(NEXT_RSC_UNION_QUERY)
).length
).toBe(1)
).toBe(0)
}
})
it('should navigate when prefetch is false', async () => {
const browser = await next.browser('/prefetch-false/initial')
await browser
.elementByCss('#to-prefetch-false-result')
.click()
.waitForElementByCss('#prefetch-false-page-result')
expect(
await browser.elementByCss('#prefetch-false-page-result').text()
).toBe('Result page')
})
it('should not need to prefetch the layout if the prefetch is initiated at the same segment', async () => {
const stateTree = encodeURIComponent(
JSON.stringify([
'',
{
children: [
'prefetch-auto',
{
children: [
['slug', 'justputit', 'd'],
{ children: ['__PAGE__', {}] },
],
},
],
},
null,
null,
true,
])
)
const response = await next.fetch(`/prefetch-auto/justputit?_rsc=dcqtr`, {
headers: {
RSC: '1',
'Next-Router-Prefetch': '1',
'Next-Router-State-Tree': stateTree,
'Next-Url': '/prefetch-auto/justputit',
},
})
it('should calculate `_rsc` query based on `Next-Url`', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
let staticPageRequests: string[] = []
const prefetchResponse = await response.text()
expect(prefetchResponse).not.toContain('Hello World')
expect(prefetchResponse).not.toContain('Loading Prefetch Auto')
})
browser.on('request', (req) => {
const url = new URL(req.url())
if (url.toString().includes(`/static-page?${NEXT_RSC_UNION_QUERY}=`)) {
staticPageRequests.push(`${url.pathname}${url.search}`)
}
it('should only prefetch the loading state and not the component tree when prefetching at the same segment', async () => {
const stateTree = encodeURIComponent(
JSON.stringify([
'',
{
children: [
'prefetch-auto',
{
children: [
['slug', 'vercel', 'd'],
{ children: ['__PAGE__', {}] },
],
},
],
},
null,
null,
true,
])
)
const response = await next.fetch(`/prefetch-auto/justputit?_rsc=dcqtr`, {
headers: {
RSC: '1',
'Next-Router-Prefetch': '1',
'Next-Router-State-Tree': stateTree,
'Next-Url': '/prefetch-auto/vercel',
},
})
const prefetchResponse = await response.text()
expect(prefetchResponse).not.toContain('Hello World')
expect(prefetchResponse).toContain('Loading Prefetch Auto')
})
it('should not re-render error component when triggering a prefetch action', async () => {
const browser = await next.browser('/with-error')
const initialRandom = await browser
.elementByCss('button')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.eval('window.next.router.prefetch("/")')
// confirm the error component was not re-rendered
expect(await browser.elementById('random-number').text()).toBe(
initialRandom
)
})
describe('dynamic rendering', () => {
describe.each(['/force-dynamic', '/revalidate-0'])('%s', (basePath) => {
it('should not re-render layout when navigating between sub-pages', async () => {
const logStartIndex = next.cliOutput.length
const browser = await next.browser(`${basePath}/test-page`)
let initialRandomNumber = await browser
.elementById('random-number')
.text()
await browser
.elementByCss(`[href="${basePath}/test-page/sub-page"]`)
.click()
await check(() => browser.hasElementByCssSelector('#sub-page'), true)
const newRandomNumber = await browser
.elementById('random-number')
.text()
expect(initialRandomNumber).toBe(newRandomNumber)
await check(() => {
const logOccurrences =
next.cliOutput.slice(logStartIndex).split('re-fetching in layout')
.length - 1
return logOccurrences
}, 1)
})
await browser.eval('location.href = "/"')
await browser.eval(
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
)
await check(() => {
return staticPageRequests.length === 1
? 'success'
: JSON.stringify(staticPageRequests)
}, 'success')
// Unable to clear router cache so mpa navigation
await browser.eval('location.href = "/dashboard"')
await browser.eval(
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
)
await check(() => {
return staticPageRequests.length === 2
? 'success'
: JSON.stringify(staticPageRequests)
}, 'success')
expect(staticPageRequests[0]).toMatch('/static-page?_rsc=')
expect(staticPageRequests[1]).toMatch('/static-page?_rsc=')
// `_rsc` does not match because it depends on the `Next-Url`
expect(staticPageRequests[0]).not.toBe(staticPageRequests[1])
})
it('should not prefetch for a bot user agent', async () => {
const browser = await next.browser('/404')
let requests: string[] = []
browser.on('request', (req) => {
requests.push(new URL(req.url()).pathname)
it('should update search params following a link click', async () => {
const browser = await next.browser(`${basePath}/search-params`)
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
await browser.elementByCss(`[href="${basePath}/search-params"]`).click()
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
})
await browser.eval(
`location.href = "/?useragent=${encodeURIComponent(
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
)}"`
)
await browser.elementByCss('#to-static-page').moveTo()
// check five times to ensure prefetch didn't occur
for (let i = 0; i < 5; i++) {
await waitFor(500)
expect(
requests.filter(
(request) =>
request === '/static-page' ||
request.includes(NEXT_RSC_UNION_QUERY)
).length
).toBe(0)
}
})
it('should navigate when prefetch is false', async () => {
const browser = await next.browser('/prefetch-false/initial')
it('should not re-fetch cached data when navigating back to a route group', async () => {
const browser = await next.browser('/prefetch-auto-route-groups')
// once the page has loaded, we expect a data fetch
expect(await browser.elementById('count').text()).toBe('1')
// once navigating to a sub-page, we expect another data fetch
await browser
.elementByCss('#to-prefetch-false-result')
.elementByCss("[href='/prefetch-auto-route-groups/sub/foo']")
.click()
.waitForElementByCss('#prefetch-false-page-result')
expect(
await browser.elementByCss('#prefetch-false-page-result').text()
).toBe('Result page')
})
// navigating back to the route group page shouldn't trigger any data fetch
await browser.elementByCss("[href='/prefetch-auto-route-groups']").click()
it('should not need to prefetch the layout if the prefetch is initiated at the same segment', async () => {
const stateTree = encodeURIComponent(
JSON.stringify([
'',
{
children: [
'prefetch-auto',
{
children: [
['slug', 'justputit', 'd'],
{ children: ['__PAGE__', {}] },
],
},
],
},
null,
null,
true,
])
)
const response = await next.fetch(`/prefetch-auto/justputit?_rsc=dcqtr`, {
headers: {
RSC: '1',
'Next-Router-Prefetch': '1',
'Next-Router-State-Tree': stateTree,
'Next-Url': '/prefetch-auto/justputit',
},
})
// confirm that the dashboard page is still rendering the stale fetch count, as it should be cached
expect(await browser.elementById('count').text()).toBe('1')
const prefetchResponse = await response.text()
expect(prefetchResponse).not.toContain('Hello World')
expect(prefetchResponse).not.toContain('Loading Prefetch Auto')
})
it('should only prefetch the loading state and not the component tree when prefetching at the same segment', async () => {
const stateTree = encodeURIComponent(
JSON.stringify([
'',
{
children: [
'prefetch-auto',
{
children: [
['slug', 'vercel', 'd'],
{ children: ['__PAGE__', {}] },
],
},
],
},
null,
null,
true,
])
)
const response = await next.fetch(`/prefetch-auto/justputit?_rsc=dcqtr`, {
headers: {
RSC: '1',
'Next-Router-Prefetch': '1',
'Next-Router-State-Tree': stateTree,
'Next-Url': '/prefetch-auto/vercel',
},
})
const prefetchResponse = await response.text()
expect(prefetchResponse).not.toContain('Hello World')
expect(prefetchResponse).toContain('Loading Prefetch Auto')
})
it('should not re-render error component when triggering a prefetch action', async () => {
const browser = await next.browser('/with-error')
const initialRandom = await browser
.elementByCss('button')
// navigating to a new sub-page, we expect another data fetch
await browser
.elementByCss("[href='/prefetch-auto-route-groups/sub/bar']")
.click()
.waitForElementByCss('#random-number')
.text()
await browser.eval('window.next.router.prefetch("/")')
// finally, going back to the route group page shouldn't trigger any data fetch
await browser.elementByCss("[href='/prefetch-auto-route-groups']").click()
// confirm the error component was not re-rendered
expect(await browser.elementById('random-number').text()).toBe(
initialRandom
)
// confirm that the dashboard page is still rendering the stale fetch count, as it should be cached
expect(await browser.elementById('count').text()).toBe('1')
await browser.refresh()
// reloading the page, we should now get an accurate total number of fetches
// the initial fetch, 2 sub-page fetches, and a final fetch when reloading the page
expect(await browser.elementById('count').text()).toBe('4')
})
describe('dynamic rendering', () => {
describe.each(['/force-dynamic', '/revalidate-0'])('%s', (basePath) => {
it('should not re-render layout when navigating between sub-pages', async () => {
const logStartIndex = next.cliOutput.length
const browser = await next.browser(`${basePath}/test-page`)
let initialRandomNumber = await browser
.elementById('random-number')
.text()
await browser
.elementByCss(`[href="${basePath}/test-page/sub-page"]`)
.click()
await check(() => browser.hasElementByCssSelector('#sub-page'), true)
const newRandomNumber = await browser
.elementById('random-number')
.text()
expect(initialRandomNumber).toBe(newRandomNumber)
await check(() => {
const logOccurrences =
next.cliOutput.slice(logStartIndex).split('re-fetching in layout')
.length - 1
return logOccurrences
}, 1)
})
it('should update search params following a link click', async () => {
const browser = await next.browser(`${basePath}/search-params`)
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
await browser
.elementByCss(`[href="${basePath}/search-params"]`)
.click()
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
})
})
it('should not re-fetch cached data when navigating back to a route group', async () => {
const browser = await next.browser('/prefetch-auto-route-groups')
// once the page has loaded, we expect a data fetch
expect(await browser.elementById('count').text()).toBe('1')
// once navigating to a sub-page, we expect another data fetch
await browser
.elementByCss("[href='/prefetch-auto-route-groups/sub/foo']")
.click()
// navigating back to the route group page shouldn't trigger any data fetch
await browser
.elementByCss("[href='/prefetch-auto-route-groups']")
.click()
// confirm that the dashboard page is still rendering the stale fetch count, as it should be cached
expect(await browser.elementById('count').text()).toBe('1')
// navigating to a new sub-page, we expect another data fetch
await browser
.elementByCss("[href='/prefetch-auto-route-groups/sub/bar']")
.click()
// finally, going back to the route group page shouldn't trigger any data fetch
await browser
.elementByCss("[href='/prefetch-auto-route-groups']")
.click()
// confirm that the dashboard page is still rendering the stale fetch count, as it should be cached
expect(await browser.elementById('count').text()).toBe('1')
await browser.refresh()
// reloading the page, we should now get an accurate total number of fetches
// the initial fetch, 2 sub-page fetches, and a final fetch when reloading the page
expect(await browser.elementById('count').text()).toBe('4')
})
})
}
)
})
})

View file

@ -1,129 +1,131 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { waitFor } from 'next-test-utils'
import cheerio from 'cheerio'
createNextDescribe(
'app dir rendering',
{
describe('app dir rendering', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextDev: isDev }) => {
it('should serve app/page.server.js at /', async () => {
const html = await next.render('/')
expect(html).toContain('app/page.server.js')
})
})
describe('SSR only', () => {
it('should run data in layout and page', async () => {
const $ = await next.render$('/ssr-only/nested')
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
})
it('should run data fetch in parallel', async () => {
const startTime = Date.now()
const $ = await next.render$('/ssr-only/slow')
const endTime = Date.now()
const duration = endTime - startTime
// Each part takes 5 seconds so it should be below 10 seconds
// Using 7 seconds to ensure external factors causing slight slowness don't fail the tests
expect(duration).toBeLessThan(10_000)
expect($('#slow-layout-message').text()).toBe('hello from slow layout')
expect($('#slow-page-message').text()).toBe('hello from slow page')
})
})
describe('static only', () => {
it('should run data in layout and page', async () => {
const $ = await next.render$('/static-only/nested')
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
})
it(`should run data in parallel ${
isDev ? 'during development' : 'and use cached version for production'
}`, async () => {
// const startTime = Date.now()
const $ = await next.render$('/static-only/slow')
// const endTime = Date.now()
// const duration = endTime - startTime
// Each part takes 5 seconds so it should be below 10 seconds
// Using 7 seconds to ensure external factors causing slight slowness don't fail the tests
// TODO: cache static props in prod
// expect(duration < (isDev ? 7000 : 2000)).toBe(true)
// expect(duration < 7000).toBe(true)
expect($('#slow-layout-message').text()).toBe('hello from slow layout')
expect($('#slow-page-message').text()).toBe('hello from slow page')
})
})
describe('ISR', () => {
it('should revalidate the page when revalidate is configured', async () => {
const getPage = async () => {
const res = await next.fetch('isr-multiple/nested')
const html = await res.text()
return {
$: cheerio.load(html),
cacheHeader: res.headers['x-nextjs-cache'],
}
}
const { $ } = await getPage()
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
const layoutNow = $('#layout-now').text()
const pageNow = $('#page-now').text()
await waitFor(2000)
// TODO: implement
// Trigger revalidate
// const { cacheHeader: revalidateCacheHeader } = await getPage()
// expect(revalidateCacheHeader).toBe('STALE')
// TODO: implement
const { $: $revalidated /* cacheHeader: revalidatedCacheHeader */ } =
await getPage()
// expect(revalidatedCacheHeader).toBe('REVALIDATED')
const layoutNowRevalidated = $revalidated('#layout-now').text()
const pageNowRevalidated = $revalidated('#page-now').text()
// Expect that the `Date.now()` is different as the page have been regenerated
expect(layoutNow).not.toBe(layoutNowRevalidated)
expect(pageNow).not.toBe(pageNowRevalidated)
})
})
// TODO: implement
describe.skip('mixed static and dynamic', () => {
it('should generate static data during build and use it', async () => {
const getPage = async () => {
const $ = await next.render$('isr-ssr-combined/nested')
return {
$,
}
}
const { $ } = await getPage()
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
const layoutNow = $('#layout-now').text()
const pageNow = $('#page-now').text()
const { $: $second } = await getPage()
const layoutNowSecond = $second('#layout-now').text()
const pageNowSecond = $second('#page-now').text()
// Expect that the `Date.now()` is different as it came from getServerSideProps
expect(layoutNow).not.toBe(layoutNowSecond)
// Expect that the `Date.now()` is the same as it came from getStaticProps
expect(pageNow).toBe(pageNowSecond)
})
})
if (skipped) {
return
}
)
it('should serve app/page.server.js at /', async () => {
const html = await next.render('/')
expect(html).toContain('app/page.server.js')
})
describe('SSR only', () => {
it('should run data in layout and page', async () => {
const $ = await next.render$('/ssr-only/nested')
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
})
it('should run data fetch in parallel', async () => {
const startTime = Date.now()
const $ = await next.render$('/ssr-only/slow')
const endTime = Date.now()
const duration = endTime - startTime
// Each part takes 5 seconds so it should be below 10 seconds
// Using 7 seconds to ensure external factors causing slight slowness don't fail the tests
expect(duration).toBeLessThan(10_000)
expect($('#slow-layout-message').text()).toBe('hello from slow layout')
expect($('#slow-page-message').text()).toBe('hello from slow page')
})
})
describe('static only', () => {
it('should run data in layout and page', async () => {
const $ = await next.render$('/static-only/nested')
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
})
it(`should run data in parallel ${
isNextDev ? 'during development' : 'and use cached version for production'
}`, async () => {
// const startTime = Date.now()
const $ = await next.render$('/static-only/slow')
// const endTime = Date.now()
// const duration = endTime - startTime
// Each part takes 5 seconds so it should be below 10 seconds
// Using 7 seconds to ensure external factors causing slight slowness don't fail the tests
// TODO: cache static props in prod
// expect(duration < (isDev ? 7000 : 2000)).toBe(true)
// expect(duration < 7000).toBe(true)
expect($('#slow-layout-message').text()).toBe('hello from slow layout')
expect($('#slow-page-message').text()).toBe('hello from slow page')
})
})
describe('ISR', () => {
it('should revalidate the page when revalidate is configured', async () => {
const getPage = async () => {
const res = await next.fetch('isr-multiple/nested')
const html = await res.text()
return {
$: cheerio.load(html),
cacheHeader: res.headers['x-nextjs-cache'],
}
}
const { $ } = await getPage()
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
const layoutNow = $('#layout-now').text()
const pageNow = $('#page-now').text()
await waitFor(2000)
// TODO: implement
// Trigger revalidate
// const { cacheHeader: revalidateCacheHeader } = await getPage()
// expect(revalidateCacheHeader).toBe('STALE')
// TODO: implement
const { $: $revalidated /* cacheHeader: revalidatedCacheHeader */ } =
await getPage()
// expect(revalidatedCacheHeader).toBe('REVALIDATED')
const layoutNowRevalidated = $revalidated('#layout-now').text()
const pageNowRevalidated = $revalidated('#page-now').text()
// Expect that the `Date.now()` is different as the page have been regenerated
expect(layoutNow).not.toBe(layoutNowRevalidated)
expect(pageNow).not.toBe(pageNowRevalidated)
})
})
// TODO: implement
describe.skip('mixed static and dynamic', () => {
it('should generate static data during build and use it', async () => {
const getPage = async () => {
const $ = await next.render$('isr-ssr-combined/nested')
return {
$,
}
}
const { $ } = await getPage()
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
const layoutNow = $('#layout-now').text()
const pageNow = $('#page-now').text()
const { $: $second } = await getPage()
const layoutNowSecond = $second('#layout-now').text()
const pageNowSecond = $second('#page-now').text()
// Expect that the `Date.now()` is different as it came from getServerSideProps
expect(layoutNow).not.toBe(layoutNowSecond)
// Expect that the `Date.now()` is the same as it came from getStaticProps
expect(pageNow).toBe(pageNowSecond)
})
})
})

View file

@ -1,18 +1,20 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
const bathPath = process.env.BASE_PATH ?? ''
createNextDescribe(
'app-routes-subrequests',
{
describe('app-routes-subrequests', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('shortcuts after 5 subrequests', async () => {
expect(JSON.parse(await next.render(bathPath + '/'))).toEqual({
count: 5,
})
})
})
if (skipped) {
return
}
)
it('shortcuts after 5 subrequests', async () => {
expect(JSON.parse(await next.render(bathPath + '/'))).toEqual({
count: 5,
})
})
})

View file

@ -1,31 +1,29 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app-routes-trailing-slash',
{
describe('app-routes-trailing-slash', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it.each(['edge', 'node'])(
'should handle trailing slash for %s runtime',
async (runtime) => {
let res = await next.fetch(`/runtime/${runtime}`, {
redirect: 'manual',
})
})
expect(res.status).toEqual(308)
expect(res.headers.get('location')).toEndWith(`/runtime/${runtime}/`)
it.each(['edge', 'node'])(
'should handle trailing slash for %s runtime',
async (runtime) => {
let res = await next.fetch(`/runtime/${runtime}`, {
redirect: 'manual',
})
res = await next.fetch(`/runtime/${runtime}/`, {
redirect: 'manual',
})
expect(res.status).toEqual(308)
expect(res.headers.get('location')).toEndWith(`/runtime/${runtime}/`)
expect(res.status).toEqual(200)
await expect(res.json()).resolves.toEqual({
url: `/runtime/${runtime}/`,
nextUrl: `/runtime/${runtime}/`,
})
}
)
}
)
res = await next.fetch(`/runtime/${runtime}/`, {
redirect: 'manual',
})
expect(res.status).toEqual(200)
await expect(res.json()).resolves.toEqual({
url: `/runtime/${runtime}/`,
nextUrl: `/runtime/${runtime}/`,
})
}
)
})

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,26 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
const bathPath = process.env.BASE_PATH ?? ''
createNextDescribe(
'app-simple-routes',
{
describe('app-simple-routes', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
describe('works with simple routes', () => {
it('renders a node route', async () => {
expect(
JSON.parse(await next.render(bathPath + '/api/node.json'))
).toEqual({
pathname: '/api/node.json',
})
})
it('renders a edge route', async () => {
expect(
JSON.parse(await next.render(bathPath + '/api/edge.json'))
).toEqual({
pathname: '/api/edge.json',
})
})
describe('works with simple routes', () => {
it('renders a node route', async () => {
expect(
JSON.parse(await next.render(bathPath + '/api/node.json'))
).toEqual({
pathname: '/api/node.json',
})
})
}
)
it('renders a edge route', async () => {
expect(
JSON.parse(await next.render(bathPath + '/api/edge.json'))
).toEqual({
pathname: '/api/edge.json',
})
})
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,30 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - validation',
{
describe('app dir - validation', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('should error when passing invalid router state tree', async () => {
const res = await next.fetch('/', {
headers: {
RSC: '1',
'Next-Router-State-Tree': JSON.stringify(['', '']),
},
})
expect(res.status).toBe(500)
})
const res2 = await next.fetch('/', {
headers: {
RSC: '1',
'Next-Router-State-Tree': JSON.stringify(['', {}]),
},
})
expect(res2.status).toBe(200)
})
if (skipped) {
return
}
)
it('should error when passing invalid router state tree', async () => {
const res = await next.fetch('/', {
headers: {
RSC: '1',
'Next-Router-State-Tree': JSON.stringify(['', '']),
},
})
expect(res.status).toBe(500)
const res2 = await next.fetch('/', {
headers: {
RSC: '1',
'Next-Router-State-Tree': JSON.stringify(['', {}]),
},
})
expect(res2.status).toBe(200)
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import fs from 'fs-extra'
import os from 'os'
import path from 'path'
@ -12,77 +12,76 @@ import {
if (!(globalThis as any).isNextStart) {
it('should skip for non-next start', () => {})
} else {
createNextDescribe(
'output: standalone with getStaticProps',
{
describe('output: standalone with getStaticProps', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
dependencies: {
swr: 'latest',
},
},
({ next }) => {
beforeAll(async () => {
await next.patchFile(
'next.config.js',
(await next.readFile('next.config.js')).replace('// output', 'output')
)
})
await next.patchFile(
'pages/gsp.js',
`
import useSWR from 'swr'
console.log(useSWR)
export default function Home() {
return <h1>Hello</h1>
}
export async function getStaticProps() {
return {
props: {
foo: "bar",
},
};
}
`
)
await next.start()
})
it('should work correctly with output standalone', async () => {
const tmpFolder = path.join(
os.tmpdir(),
'next-standalone-' + Date.now()
)
await fs.move(path.join(next.testDir, '.next/standalone'), tmpFolder)
let server: any
try {
const testServer = path.join(tmpFolder, 'server.js')
const appPort = await findPort()
server = await initNextServerScript(
testServer,
/- Local:/,
{
...process.env,
PORT: appPort.toString(),
},
undefined,
{
cwd: tmpFolder,
}
)
const res = await fetchViaHTTP(appPort, '/gsp')
expect(res.status).toBe(200)
} finally {
if (server) await killApp(server)
await fs.remove(tmpFolder)
}
})
if (skipped) {
return
}
)
beforeAll(async () => {
await next.patchFile(
'next.config.js',
(await next.readFile('next.config.js')).replace('// output', 'output')
)
await next.patchFile(
'pages/gsp.js',
`
import useSWR from 'swr'
console.log(useSWR)
export default function Home() {
return <h1>Hello</h1>
}
export async function getStaticProps() {
return {
props: {
foo: "bar",
},
};
}
`
)
await next.start()
})
it('should work correctly with output standalone', async () => {
const tmpFolder = path.join(os.tmpdir(), 'next-standalone-' + Date.now())
await fs.move(path.join(next.testDir, '.next/standalone'), tmpFolder)
let server: any
try {
const testServer = path.join(tmpFolder, 'server.js')
const appPort = await findPort()
server = await initNextServerScript(
testServer,
/- Local:/,
{
...process.env,
PORT: appPort.toString(),
},
undefined,
{
cwd: tmpFolder,
}
)
const res = await fetchViaHTTP(appPort, '/gsp')
expect(res.status).toBe(200)
} finally {
if (server) await killApp(server)
await fs.remove(tmpFolder)
}
})
})
}

View file

@ -1,4 +1,4 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import fs from 'fs-extra'
import os from 'os'
import path from 'path'
@ -12,93 +12,92 @@ import {
if (!(globalThis as any).isNextStart) {
it('should skip for non-next start', () => {})
} else {
createNextDescribe(
'output: standalone with app dir',
{
describe('output: standalone with app dir', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
},
({ next }) => {
beforeAll(async () => {
await next.patchFile(
'next.config.js',
(await next.readFile('next.config.js')).replace('// output', 'output')
)
await next.start()
})
})
it('should handle trace files correctly for route groups (nodejs only)', async () => {
expect(next.cliOutput).not.toContain('Failed to copy traced files')
const serverDirPath = path.join(
next.testDir,
'.next/standalone/.next/server'
)
for (const page of [
'(newroot)/dashboard/another',
'(newroot)/dashboard/project/[projectId]',
'(rootonly)/dashboard/changelog',
]) {
const pagePath = path.join(serverDirPath, 'app', page)
expect(
await fs.pathExists(path.join(pagePath, 'page.js.nft.json'))
).toBe(true)
const files = (
await fs.readJSON(path.join(pagePath, 'page.js.nft.json'))
).files as string[]
for (const file of files) {
expect(await fs.pathExists(path.join(pagePath, file))).toBe(true)
}
}
})
it('should work correctly with output standalone', async () => {
const tmpFolder = path.join(
os.tmpdir(),
'next-standalone-' + Date.now()
)
await fs.move(path.join(next.testDir, '.next/standalone'), tmpFolder)
let server: any
try {
const testServer = path.join(tmpFolder, 'server.js')
const appPort = await findPort()
server = await initNextServerScript(
testServer,
/- Local:/,
{
...process.env,
PORT: appPort.toString(),
},
undefined,
{
cwd: tmpFolder,
}
)
for (const testPath of [
'/',
'/api/hello',
'/blog/first',
'/dashboard',
'/dashboard/another',
'/dashboard/changelog',
'/dashboard/deployments/breakdown',
'/dashboard/deployments/123',
'/dashboard/hello',
'/dashboard/project/123',
'/catch-all/first',
]) {
const res = await fetchViaHTTP(appPort, testPath)
expect(res.status).toBe(200)
}
} finally {
if (server) await killApp(server)
await fs.remove(tmpFolder)
}
})
if (skipped) {
return
}
)
beforeAll(async () => {
await next.patchFile(
'next.config.js',
(await next.readFile('next.config.js')).replace('// output', 'output')
)
await next.start()
})
it('should handle trace files correctly for route groups (nodejs only)', async () => {
expect(next.cliOutput).not.toContain('Failed to copy traced files')
const serverDirPath = path.join(
next.testDir,
'.next/standalone/.next/server'
)
for (const page of [
'(newroot)/dashboard/another',
'(newroot)/dashboard/project/[projectId]',
'(rootonly)/dashboard/changelog',
]) {
const pagePath = path.join(serverDirPath, 'app', page)
expect(
await fs.pathExists(path.join(pagePath, 'page.js.nft.json'))
).toBe(true)
const files = (
await fs.readJSON(path.join(pagePath, 'page.js.nft.json'))
).files as string[]
for (const file of files) {
expect(await fs.pathExists(path.join(pagePath, file))).toBe(true)
}
}
})
it('should work correctly with output standalone', async () => {
const tmpFolder = path.join(os.tmpdir(), 'next-standalone-' + Date.now())
await fs.move(path.join(next.testDir, '.next/standalone'), tmpFolder)
let server: any
try {
const testServer = path.join(tmpFolder, 'server.js')
const appPort = await findPort()
server = await initNextServerScript(
testServer,
/- Local:/,
{
...process.env,
PORT: appPort.toString(),
},
undefined,
{
cwd: tmpFolder,
}
)
for (const testPath of [
'/',
'/api/hello',
'/blog/first',
'/dashboard',
'/dashboard/another',
'/dashboard/changelog',
'/dashboard/deployments/breakdown',
'/dashboard/deployments/123',
'/dashboard/hello',
'/dashboard/project/123',
'/catch-all/first',
]) {
const res = await fetchViaHTTP(appPort, testPath)
expect(res.status).toBe(200)
}
} finally {
if (server) await killApp(server)
await fs.remove(tmpFolder)
}
})
})
}

View file

@ -1,17 +1,17 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'async-component-preload',
{
describe('async-component-preload', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('should handle redirect in an async page', async () => {
const browser = await next.browser('/')
expect(await browser.waitForElementByCss('#success').text()).toBe(
'Success'
)
})
})
if (skipped) {
return
}
)
it('should handle redirect in an async page', async () => {
const browser = await next.browser('/')
expect(await browser.waitForElementByCss('#success').text()).toBe('Success')
})
})

View file

@ -1,60 +1,58 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'router autoscrolling on navigation with css modules',
{
describe('router autoscrolling on navigation with css modules', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
type BrowserInterface = Awaited<ReturnType<(typeof next)['browser']>>
})
const getTopScroll = async (browser: BrowserInterface) =>
await browser.eval('document.documentElement.scrollTop')
type BrowserInterface = Awaited<ReturnType<(typeof next)['browser']>>
const getLeftScroll = async (browser: BrowserInterface) =>
await browser.eval('document.documentElement.scrollLeft')
const getTopScroll = async (browser: BrowserInterface) =>
await browser.eval('document.documentElement.scrollTop')
const waitForScrollToComplete = (
browser,
options: { x: number; y: number }
) =>
check(async () => {
const top = await getTopScroll(browser)
const left = await getLeftScroll(browser)
return top === options.y && left === options.x
? 'success'
: JSON.stringify({ top, left })
}, 'success')
const getLeftScroll = async (browser: BrowserInterface) =>
await browser.eval('document.documentElement.scrollLeft')
const scrollTo = async (
browser: BrowserInterface,
options: { x: number; y: number }
) => {
await browser.eval(`window.scrollTo(${options.x}, ${options.y})`)
await waitForScrollToComplete(browser, options)
}
const waitForScrollToComplete = (
browser,
options: { x: number; y: number }
) =>
check(async () => {
const top = await getTopScroll(browser)
const left = await getLeftScroll(browser)
return top === options.y && left === options.x
? 'success'
: JSON.stringify({ top, left })
}, 'success')
describe('vertical scroll when page imports css modules', () => {
it('should scroll to top of document when navigating between to pages without layout when', async () => {
const browser: BrowserInterface = await next.browser('/1')
await scrollTo(browser, { x: 0, y: 1000 })
expect(await getTopScroll(browser)).toBe(1000)
await browser.elementById('lower').click()
await waitForScrollToComplete(browser, { x: 0, y: 0 })
})
it('should scroll when clicking in JS', async () => {
const browser: BrowserInterface = await next.browser('/1')
await scrollTo(browser, { x: 0, y: 1000 })
expect(await getTopScroll(browser)).toBe(1000)
await browser.eval(() => document.getElementById('lower').click())
await waitForScrollToComplete(browser, { x: 0, y: 0 })
})
})
const scrollTo = async (
browser: BrowserInterface,
options: { x: number; y: number }
) => {
await browser.eval(`window.scrollTo(${options.x}, ${options.y})`)
await waitForScrollToComplete(browser, options)
}
)
describe('vertical scroll when page imports css modules', () => {
it('should scroll to top of document when navigating between to pages without layout when', async () => {
const browser: BrowserInterface = await next.browser('/1')
await scrollTo(browser, { x: 0, y: 1000 })
expect(await getTopScroll(browser)).toBe(1000)
await browser.elementById('lower').click()
await waitForScrollToComplete(browser, { x: 0, y: 0 })
})
it('should scroll when clicking in JS', async () => {
const browser: BrowserInterface = await next.browser('/1')
await scrollTo(browser, { x: 0, y: 1000 })
expect(await getTopScroll(browser)).toBe(1000)
await browser.eval(() => document.getElementById('lower').click())
await waitForScrollToComplete(browser, { x: 0, y: 0 })
})
})
})

View file

@ -1,29 +1,31 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
// TODO-APP: fix test as it's failing randomly
describe.skip('app-dir back button download bug', () => {
createNextDescribe(
'app-dir back button download bug',
{
describe('app-dir back button download bug', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('should redirect route when clicking link', async () => {
const browser = await next.browser('/')
const text = await browser
.elementByCss('#to-post-1')
.click()
.waitForElementByCss('#post-page')
.text()
expect(text).toBe('This is the post page')
})
await browser.back()
expect(await browser.waitForElementByCss('#home-page').text()).toBe(
'Home!'
)
})
if (skipped) {
return
}
)
it('should redirect route when clicking link', async () => {
const browser = await next.browser('/')
const text = await browser
.elementByCss('#to-post-1')
.click()
.waitForElementByCss('#post-page')
.text()
expect(text).toBe('This is the post page')
await browser.back()
expect(await browser.waitForElementByCss('#home-page').text()).toBe(
'Home!'
)
})
})
})

View file

@ -1,49 +1,51 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app-dir build size',
{
describe('app-dir build size', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextStart }) => {
if (isNextStart) {
it('should have correct size in build output', async () => {
const regex = /(\S+)\s+([\d.]+\s\w+)\s+([\d.]+\s\w+)/g
const matches = [...next.cliOutput.matchAll(regex)]
})
const result = matches.reduce((acc, match) => {
const [, path, size, firstLoadJS] = match
acc[path] = { size, firstLoadJS }
return acc
}, {})
// convert pretty-bytes format into bytes so we can compare units
const sizeToBytes = (size: string) => {
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const [value, unit] = size.split(' ', 2)
const exp = units.indexOf(unit)
return parseFloat(value) * Math.pow(1024, exp)
}
const index = result['/']
const foo = result['/foo']
// index route has a page, so it should not be 0
expect(sizeToBytes(index.size)).toBeGreaterThan(0)
expect(sizeToBytes(index.firstLoadJS)).toBeGreaterThan(0)
// foo route has a page, so it should not be 0
expect(sizeToBytes(foo.size)).toBeGreaterThan(0)
expect(sizeToBytes(foo.firstLoadJS)).toBeGreaterThan(0)
// foo is a client component, so it should be larger than index
expect(sizeToBytes(foo.size)).toBeGreaterThan(sizeToBytes(index.size))
})
} else {
it('should skip next dev for now', () => {})
return
}
if (skipped) {
return
}
)
if (isNextStart) {
it('should have correct size in build output', async () => {
const regex = /(\S+)\s+([\d.]+\s\w+)\s+([\d.]+\s\w+)/g
const matches = [...next.cliOutput.matchAll(regex)]
const result = matches.reduce((acc, match) => {
const [, path, size, firstLoadJS] = match
acc[path] = { size, firstLoadJS }
return acc
}, {})
// convert pretty-bytes format into bytes so we can compare units
const sizeToBytes = (size: string) => {
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const [value, unit] = size.split(' ', 2)
const exp = units.indexOf(unit)
return parseFloat(value) * Math.pow(1024, exp)
}
const index = result['/']
const foo = result['/foo']
// index route has a page, so it should not be 0
expect(sizeToBytes(index.size)).toBeGreaterThan(0)
expect(sizeToBytes(index.firstLoadJS)).toBeGreaterThan(0)
// foo route has a page, so it should not be 0
expect(sizeToBytes(foo.size)).toBeGreaterThan(0)
expect(sizeToBytes(foo.firstLoadJS)).toBeGreaterThan(0)
// foo is a client component, so it should be larger than index
expect(sizeToBytes(foo.size)).toBeGreaterThan(sizeToBytes(index.size))
})
} else {
it('should skip next dev for now', () => {})
return
}
})

View file

@ -1,31 +1,33 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'conflicting-page-segments',
{
describe('conflicting-page-segments', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
// we skip start because the build will fail and we won't be able to catch it
// start is re-triggered but caught in the assertions below
skipStart: true,
},
({ next, isNextDev }) => {
it('should throw an error when a route groups causes a conflict with a parallel segment', async () => {
if (isNextDev) {
await next.start()
const html = await next.render('/')
})
expect(html).toContain(
'You cannot have two parallel pages that resolve to the same path.'
)
} else {
await expect(next.start()).rejects.toThrow('next build failed')
await check(
() => next.cliOutput,
/You cannot have two parallel pages that resolve to the same path\. Please check \/\(group-a\)\/page and \/\(group-b\)\/page\./i
)
}
})
if (skipped) {
return
}
)
it('should throw an error when a route groups causes a conflict with a parallel segment', async () => {
if (isNextDev) {
await next.start()
const html = await next.render('/')
expect(html).toContain(
'You cannot have two parallel pages that resolve to the same path.'
)
} else {
await expect(next.start()).rejects.toThrow('next build failed')
await check(
() => next.cliOutput,
/You cannot have two parallel pages that resolve to the same path\. Please check \/\(group-a\)\/page and \/\(group-b\)\/page\./i
)
}
})
})

View file

@ -1,22 +1,20 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'Web Crypto API is available globally',
{
describe('Web Crypto API is available globally', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
// Recommended for tests that need a full browser
it('should be available in Server Components', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('crypto is available')
})
})
// In case you need to test the response object
it('should be available in Route Handlers', async () => {
const res = await next.fetch('/handler')
const html = await res.text()
expect(html).toContain('crypto is available')
})
}
)
// Recommended for tests that need a full browser
it('should be available in Server Components', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('crypto is available')
})
// In case you need to test the response object
it('should be available in Route Handlers', async () => {
const res = await next.fetch('/handler')
const html = await res.text()
expect(html).toContain('crypto is available')
})
})

View file

@ -1,120 +1,118 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { waitFor } from 'next-test-utils'
createNextDescribe(
'app dir - draft mode',
{
describe('app dir - draft mode', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
},
({ next, isNextDev }) => {
async function runTests({ basePath = '/' }: { basePath: string }) {
let origRandHome = 'unintialized'
let origRandWithCookies = 'unintialized'
let Cookie = ''
})
it(`should use initial rand when draft mode is disabled on ${basePath}index`, async () => {
const $ = await next.render$(basePath)
expect($('#mode').text()).toBe('DISABLED')
expect($('#rand').text()).toBeDefined()
origRandHome = $('#rand').text()
})
async function runTests({ basePath = '/' }: { basePath: string }) {
let origRandHome = 'unintialized'
let origRandWithCookies = 'unintialized'
let Cookie = ''
it(`should use initial rand when draft mode is disabled on ${basePath}with-cookies`, async () => {
const $ = await next.render$(`${basePath}with-cookies`)
expect($('#mode').text()).toBe('DISABLED')
expect($('#rand').text()).toBeDefined()
expect($('#data').text()).toBe('')
origRandWithCookies = $('#rand').text()
})
it(`should use initial rand when draft mode is disabled on ${basePath}index`, async () => {
const $ = await next.render$(basePath)
expect($('#mode').text()).toBe('DISABLED')
expect($('#rand').text()).toBeDefined()
origRandHome = $('#rand').text()
})
if (!isNextDev) {
if (basePath === '/') {
it('should not generate rand when draft mode disabled during next start', async () => {
const $ = await next.render$(basePath)
expect($('#mode').text()).toBe('DISABLED')
expect($('#rand').text()).toBe(origRandHome)
})
}
it(`should use initial rand when draft mode is disabled on ${basePath}with-cookies`, async () => {
const $ = await next.render$(`${basePath}with-cookies`)
expect($('#mode').text()).toBe('DISABLED')
expect($('#rand').text()).toBeDefined()
expect($('#data').text()).toBe('')
origRandWithCookies = $('#rand').text()
})
it('should not read other cookies when draft mode disabled during next start', async () => {
const opts = { headers: { Cookie: `data=cool` } }
const $ = await next.render$(`${basePath}with-cookies`, {}, opts)
if (!isNextDev) {
if (basePath === '/') {
it('should not generate rand when draft mode disabled during next start', async () => {
const $ = await next.render$(basePath)
expect($('#mode').text()).toBe('DISABLED')
expect($('#data').text()).toBe('')
expect($('#rand').text()).toBe(origRandHome)
})
}
it('should be disabled from api route handler', async () => {
const res = await next.fetch(`${basePath}state`)
expect(await res.text()).toBe('DISABLED')
})
it('should have set-cookie header on enable', async () => {
const res = await next.fetch(`${basePath}enable`)
const h = res.headers.get('set-cookie') || ''
Cookie = h.split(';').find((c) => c.startsWith('__prerender_bypass'))
expect(Cookie).toBeDefined()
})
it('should have set-cookie header with redirect location', async () => {
const res = await next.fetch(`${basePath}enable-and-redirect`, {
redirect: 'manual',
})
expect(res.status).toBe(307)
expect(res.headers.get('location')).toContain('/some-other-page')
const h = res.headers.get('set-cookie') || ''
const c = h.split(';').find((c) => c.startsWith('__prerender_bypass'))
expect(c).toBeDefined()
})
it('should genenerate rand when draft mode enabled', async () => {
const opts = { headers: { Cookie } }
const $ = await next.render$(basePath, {}, opts)
expect($('#mode').text()).toBe('ENABLED')
expect($('#rand').text()).not.toBe(origRandHome)
})
it('should read other cookies when draft mode enabled', async () => {
const opts = { headers: { Cookie: `${Cookie};data=cool` } }
it('should not read other cookies when draft mode disabled during next start', async () => {
const opts = { headers: { Cookie: `data=cool` } }
const $ = await next.render$(`${basePath}with-cookies`, {}, opts)
expect($('#mode').text()).toBe('ENABLED')
expect($('#rand').text()).not.toBe(origRandWithCookies)
expect($('#data').text()).toBe('cool')
})
it('should be enabled from api route handler when draft mode enabled', async () => {
const opts = { headers: { Cookie } }
const res = await next.fetch(`${basePath}state`, opts)
expect(await res.text()).toBe('ENABLED')
})
it('should not perform full page navigation on router.refresh()', async () => {
const to = encodeURIComponent('/generate/foo')
const browser = await next.browser(
`${basePath}enable-and-redirect?to=${to}`
)
await browser.eval('window._test = 42')
await browser.elementById('refresh').click()
const start = Date.now()
while (Date.now() - start < 5000) {
const value = await browser.eval('window._test')
if (value !== 42) {
throw new Error('Detected a full page navigation')
}
await waitFor(200)
}
expect(await browser.eval('window._test')).toBe(42)
expect($('#mode').text()).toBe('DISABLED')
expect($('#data').text()).toBe('')
})
}
describe('in nodejs runtime', () => {
runTests({ basePath: '/' })
it('should be disabled from api route handler', async () => {
const res = await next.fetch(`${basePath}state`)
expect(await res.text()).toBe('DISABLED')
})
describe('in edge runtime', () => {
runTests({ basePath: '/with-edge/' })
it('should have set-cookie header on enable', async () => {
const res = await next.fetch(`${basePath}enable`)
const h = res.headers.get('set-cookie') || ''
Cookie = h.split(';').find((c) => c.startsWith('__prerender_bypass'))
expect(Cookie).toBeDefined()
})
it('should have set-cookie header with redirect location', async () => {
const res = await next.fetch(`${basePath}enable-and-redirect`, {
redirect: 'manual',
})
expect(res.status).toBe(307)
expect(res.headers.get('location')).toContain('/some-other-page')
const h = res.headers.get('set-cookie') || ''
const c = h.split(';').find((c) => c.startsWith('__prerender_bypass'))
expect(c).toBeDefined()
})
it('should genenerate rand when draft mode enabled', async () => {
const opts = { headers: { Cookie } }
const $ = await next.render$(basePath, {}, opts)
expect($('#mode').text()).toBe('ENABLED')
expect($('#rand').text()).not.toBe(origRandHome)
})
it('should read other cookies when draft mode enabled', async () => {
const opts = { headers: { Cookie: `${Cookie};data=cool` } }
const $ = await next.render$(`${basePath}with-cookies`, {}, opts)
expect($('#mode').text()).toBe('ENABLED')
expect($('#rand').text()).not.toBe(origRandWithCookies)
expect($('#data').text()).toBe('cool')
})
it('should be enabled from api route handler when draft mode enabled', async () => {
const opts = { headers: { Cookie } }
const res = await next.fetch(`${basePath}state`, opts)
expect(await res.text()).toBe('ENABLED')
})
it('should not perform full page navigation on router.refresh()', async () => {
const to = encodeURIComponent('/generate/foo')
const browser = await next.browser(
`${basePath}enable-and-redirect?to=${to}`
)
await browser.eval('window._test = 42')
await browser.elementById('refresh').click()
const start = Date.now()
while (Date.now() - start < 5000) {
const value = await browser.eval('window._test')
if (value !== 42) {
throw new Error('Detected a full page navigation')
}
await waitFor(200)
}
expect(await browser.eval('window._test')).toBe(42)
})
}
)
describe('in nodejs runtime', () => {
runTests({ basePath: '/' })
})
describe('in edge runtime', () => {
runTests({ basePath: '/with-edge/' })
})
})

View file

@ -1,73 +1,75 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
createNextDescribe(
'app dir - dynamic css',
{
describe('app dir - dynamic css', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('should preload css of dynamic component during SSR', async () => {
const $ = await next.render$('/ssr')
const cssLinks = $('link[rel="stylesheet"]')
expect(cssLinks.attr('href')).toContain('.css')
})
})
it('should only apply corresponding css for page loaded that /ssr', async () => {
const browser = await next.browser('/ssr')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('0px')
})
})
it('should only apply corresponding css for page loaded in edge runtime', async () => {
const browser = await next.browser('/ssr/edge')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('0px')
})
})
it('should only apply corresponding css for page loaded that /another', async () => {
const browser = await next.browser('/another')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).not.toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('1px')
})
})
it('should not throw with accessing to ALS in preload css', async () => {
const output = next.cliOutput
expect(output).not.toContain('was called outside a request scope')
})
if (skipped) {
return
}
)
it('should preload css of dynamic component during SSR', async () => {
const $ = await next.render$('/ssr')
const cssLinks = $('link[rel="stylesheet"]')
expect(cssLinks.attr('href')).toContain('.css')
})
it('should only apply corresponding css for page loaded that /ssr', async () => {
const browser = await next.browser('/ssr')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('0px')
})
})
it('should only apply corresponding css for page loaded in edge runtime', async () => {
const browser = await next.browser('/ssr/edge')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('0px')
})
})
it('should only apply corresponding css for page loaded that /another', async () => {
const browser = await next.browser('/another')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).not.toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('1px')
})
})
it('should not throw with accessing to ALS in preload css', async () => {
const output = next.cliOutput
expect(output).not.toContain('was called outside a request scope')
})
})

View file

@ -1,306 +1,312 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { getRedboxHeader, hasRedbox } from 'next-test-utils'
process.env.__TEST_SENTINEL = 'build'
createNextDescribe(
'dynamic-data',
{
describe('dynamic-data', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname + '/fixtures/main',
skipStart: true,
skipDeployment: true,
},
({ next, isNextDev }) => {
beforeAll(async () => {
await next.start()
// This will update the __TEST_SENTINEL value to "run"
await next.render('/setenv?value=run')
})
})
it('should render the dynamic apis dynamically when used in a top-level scope', async () => {
const $ = await next.render$(
'/top-level?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// in PPR we expect the shell to be rendered at build and the page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('run')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).not.toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
}
expect($('#headers .fooheader').text()).toBe('foo header value')
expect($('#cookies .foocookie').text()).toBe('foo cookie value')
expect($('#searchparams .foo').text()).toBe('foosearch')
})
it('should render the dynamic apis dynamically when used in a top-level scope with force dynamic', async () => {
const $ = await next.render$(
'/force-dynamic?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// @TODO this should actually be build but there is a bug in how we do segment level dynamic in PPR at the moment
// see note in create-component-tree
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
}
expect($('#headers .fooheader').text()).toBe('foo header value')
expect($('#cookies .foocookie').text()).toBe('foo cookie value')
expect($('#searchparams .foo').text()).toBe('foosearch')
})
it('should render empty objects for dynamic APIs when rendering with force-static', async () => {
const $ = await next.render$(
'/force-static?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// in PPR we expect the shell to be rendered at build and the page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('build')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('build')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
}
expect($('#headers .fooheader').html()).toBeNull()
expect($('#cookies .foocookie').html()).toBeNull()
expect($('#searchparams .foo').html()).toBeNull()
})
it('should track searchParams access as dynamic when the Page is a client component', async () => {
const $ = await next.render$(
'/client-page?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we don't assert the state of the fallback because it can depend on the timing
// of when streaming starts and how fast the client references resolve
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// in PPR we expect the shell to be rendered at build and the page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('run')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).not.toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we don't assert the state of the fallback because it can depend on the timing
// of when streaming starts and how fast the client references resolve
}
expect($('#searchparams .foo').text()).toBe('foosearch')
})
if (!isNextDev) {
it('should track dynamic apis when rendering app routes', async () => {
expect(next.cliOutput).toContain(
`Caught Error: Dynamic server usage: Route /routes/url couldn't be rendered statically because it accessed \`request.url\`.`
)
expect(next.cliOutput).toContain(
`Caught Error: Dynamic server usage: Route /routes/next-url couldn't be rendered statically because it accessed \`nextUrl.toString\`.`
)
})
}
if (skipped) {
return
}
)
createNextDescribe(
'dynamic-data with dynamic = "error"',
{
beforeAll(async () => {
await next.start()
// This will update the __TEST_SENTINEL value to "run"
await next.render('/setenv?value=run')
})
it('should render the dynamic apis dynamically when used in a top-level scope', async () => {
const $ = await next.render$(
'/top-level?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// in PPR we expect the shell to be rendered at build and the page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('run')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).not.toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
}
expect($('#headers .fooheader').text()).toBe('foo header value')
expect($('#cookies .foocookie').text()).toBe('foo cookie value')
expect($('#searchparams .foo').text()).toBe('foosearch')
})
it('should render the dynamic apis dynamically when used in a top-level scope with force dynamic', async () => {
const $ = await next.render$(
'/force-dynamic?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// @TODO this should actually be build but there is a bug in how we do segment level dynamic in PPR at the moment
// see note in create-component-tree
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
}
expect($('#headers .fooheader').text()).toBe('foo header value')
expect($('#cookies .foocookie').text()).toBe('foo cookie value')
expect($('#searchparams .foo').text()).toBe('foosearch')
})
it('should render empty objects for dynamic APIs when rendering with force-static', async () => {
const $ = await next.render$(
'/force-static?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// in PPR we expect the shell to be rendered at build and the page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('build')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('build')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
}
expect($('#headers .fooheader').html()).toBeNull()
expect($('#cookies .foocookie').html()).toBeNull()
expect($('#searchparams .foo').html()).toBeNull()
})
it('should track searchParams access as dynamic when the Page is a client component', async () => {
const $ = await next.render$(
'/client-page?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we don't assert the state of the fallback because it can depend on the timing
// of when streaming starts and how fast the client references resolve
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) {
// in PPR we expect the shell to be rendered at build and the page to be rendered at runtime
expect($('#layout').text()).toBe('build')
expect($('#page').text()).toBe('run')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).not.toBeNull()
} else {
// in static generation we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('run')
expect($('#page').text()).toBe('run')
// we don't assert the state of the fallback because it can depend on the timing
// of when streaming starts and how fast the client references resolve
}
expect($('#searchparams .foo').text()).toBe('foosearch')
})
if (!isNextDev) {
it('should track dynamic apis when rendering app routes', async () => {
expect(next.cliOutput).toContain(
`Caught Error: Dynamic server usage: Route /routes/url couldn't be rendered statically because it accessed \`request.url\`.`
)
expect(next.cliOutput).toContain(
`Caught Error: Dynamic server usage: Route /routes/next-url couldn't be rendered statically because it accessed \`nextUrl.toString\`.`
)
})
}
})
describe('dynamic-data with dynamic = "error"', () => {
const { next, isNextDev, isNextDeploy, skipped } = nextTestSetup({
files: __dirname + '/fixtures/require-static',
skipStart: true,
},
({ next, isNextDev, isNextDeploy }) => {
if (isNextDeploy) {
it.skip('should not run in next deploy.', () => {})
return
}
})
if (isNextDev) {
beforeAll(async () => {
await next.start()
})
if (skipped) {
return
}
it('displays redbox when `dynamic = "error"` and dynamic data is read in dev', async () => {
let browser = await next.browser('/cookies?foo=foosearch')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /cookies with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`'
)
} finally {
await browser.close()
}
if (isNextDeploy) {
it.skip('should not run in next deploy.', () => {})
return
}
browser = await next.browser('/headers?foo=foosearch')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /headers with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`'
)
} finally {
await browser.close()
}
if (isNextDev) {
beforeAll(async () => {
await next.start()
})
browser = await next.browser('/search?foo=foosearch')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /search with `dynamic = "error"` couldn\'t be rendered statically because it used `searchParams`'
)
} finally {
await browser.close()
}
})
} else {
it('error when the build when `dynamic = "error"` and dynamic data is read', async () => {
try {
await next.start()
} catch (err) {
// We expect this to fail
}
// Error: Page with `dynamic = "error"` couldn't be rendered statically because it used `headers`
expect(next.cliOutput).toMatch(
it('displays redbox when `dynamic = "error"` and dynamic data is read in dev', async () => {
let browser = await next.browser('/cookies?foo=foosearch')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /cookies with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`'
)
expect(next.cliOutput).toMatch(
} finally {
await browser.close()
}
browser = await next.browser('/headers?foo=foosearch')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /headers with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`'
)
expect(next.cliOutput).toMatch(
'Error: Route /search with `dynamic = "error"` couldn\'t be rendered statically because it used `searchParams`.'
)
expect(next.cliOutput).toMatch(
'Error: Route /routes/form-data/error with `dynamic = "error"` couldn\'t be rendered statically because it accessed `request.formData`.'
)
expect(next.cliOutput).toMatch(
'Error: Route /routes/next-url/error with `dynamic = "error"` couldn\'t be rendered statically because it accessed `nextUrl.toString`.'
)
})
}
}
)
} finally {
await browser.close()
}
createNextDescribe(
'dynamic-data inside cache scope',
{
browser = await next.browser('/search?foo=foosearch')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /search with `dynamic = "error"` couldn\'t be rendered statically because it used `searchParams`'
)
} finally {
await browser.close()
}
})
} else {
it('error when the build when `dynamic = "error"` and dynamic data is read', async () => {
try {
await next.start()
} catch (err) {
// We expect this to fail
}
// Error: Page with `dynamic = "error"` couldn't be rendered statically because it used `headers`
expect(next.cliOutput).toMatch(
'Error: Route /cookies with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`'
)
expect(next.cliOutput).toMatch(
'Error: Route /headers with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`'
)
expect(next.cliOutput).toMatch(
'Error: Route /search with `dynamic = "error"` couldn\'t be rendered statically because it used `searchParams`.'
)
expect(next.cliOutput).toMatch(
'Error: Route /routes/form-data/error with `dynamic = "error"` couldn\'t be rendered statically because it accessed `request.formData`.'
)
expect(next.cliOutput).toMatch(
'Error: Route /routes/next-url/error with `dynamic = "error"` couldn\'t be rendered statically because it accessed `nextUrl.toString`.'
)
})
}
})
describe('dynamic-data inside cache scope', () => {
const { next, isNextDev, isNextDeploy, skipped } = nextTestSetup({
files: __dirname + '/fixtures/cache-scoped',
skipStart: true,
},
({ next, isNextDev, isNextDeploy }) => {
if (isNextDeploy) {
it.skip('should not run in next deploy..', () => {})
return
}
})
if (isNextDev) {
beforeAll(async () => {
await next.start()
})
if (skipped) {
return
}
it('displays redbox when accessing dynamic data inside a cache scope', async () => {
let browser = await next.browser('/cookies')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /cookies used "cookies" inside a function cached with "unstable_cache(...)".'
)
} finally {
await browser.close()
}
if (isNextDeploy) {
it.skip('should not run in next deploy..', () => {})
return
}
browser = await next.browser('/headers')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /headers used "headers" inside a function cached with "unstable_cache(...)".'
)
} finally {
await browser.close()
}
})
} else {
it('error when the build when accessing dynamic data inside a cache scope', async () => {
try {
await next.start()
} catch (err) {
// We expect this to fail
}
expect(next.cliOutput).toMatch(
if (isNextDev) {
beforeAll(async () => {
await next.start()
})
it('displays redbox when accessing dynamic data inside a cache scope', async () => {
let browser = await next.browser('/cookies')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /cookies used "cookies" inside a function cached with "unstable_cache(...)".'
)
expect(next.cliOutput).toMatch(
} finally {
await browser.close()
}
browser = await next.browser('/headers')
try {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /headers used "headers" inside a function cached with "unstable_cache(...)".'
)
})
}
} finally {
await browser.close()
}
})
} else {
it('error when the build when accessing dynamic data inside a cache scope', async () => {
try {
await next.start()
} catch (err) {
// We expect this to fail
}
expect(next.cliOutput).toMatch(
'Error: Route /cookies used "cookies" inside a function cached with "unstable_cache(...)".'
)
expect(next.cliOutput).toMatch(
'Error: Route /headers used "headers" inside a function cached with "unstable_cache(...)".'
)
})
}
)
})

View file

@ -1,62 +1,66 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { getRedboxDescription, hasRedbox } from 'next-test-utils'
createNextDescribe(
'dynamic-href',
{
describe('dynamic-href', () => {
const {
isNextDev: isDev,
next,
skipped,
} = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ isNextDev: isDev, next }) => {
if (isDev) {
it('should error when using dynamic href.pathname in app dir', async () => {
const browser = await next.browser('/object')
})
// Error should show up
expect(await hasRedbox(browser)).toBeTrue()
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"Error: Dynamic href \`/object/[slug]\` found in <Link> while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"`
)
// Fix error
const pageContent = await next.readFile('app/object/page.js')
await next.patchFile(
'app/object/page.js',
pageContent.replace(
"pathname: '/object/[slug]'",
"pathname: '/object/slug'"
)
)
expect(await browser.waitForElementByCss('#link').text()).toBe(
'to slug'
)
// Navigate to new page
await browser.elementByCss('#link').click()
expect(await browser.waitForElementByCss('#pathname').text()).toBe(
'/object/slug'
)
expect(await browser.elementByCss('#slug').text()).toBe('1')
})
it('should error when using dynamic href in app dir', async () => {
const browser = await next.browser('/string')
// Error should show up
expect(await hasRedbox(browser)).toBeTrue()
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"Error: Dynamic href \`/object/[slug]\` found in <Link> while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"`
)
})
} else {
it('should not error on /object in prod', async () => {
const browser = await next.browser('/object')
expect(await browser.elementByCss('#link').text()).toBe('to slug')
})
it('should not error on /string in prod', async () => {
const browser = await next.browser('/string')
expect(await browser.elementByCss('#link').text()).toBe('to slug')
})
}
if (skipped) {
return
}
)
if (isDev) {
it('should error when using dynamic href.pathname in app dir', async () => {
const browser = await next.browser('/object')
// Error should show up
expect(await hasRedbox(browser)).toBeTrue()
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"Error: Dynamic href \`/object/[slug]\` found in <Link> while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"`
)
// Fix error
const pageContent = await next.readFile('app/object/page.js')
await next.patchFile(
'app/object/page.js',
pageContent.replace(
"pathname: '/object/[slug]'",
"pathname: '/object/slug'"
)
)
expect(await browser.waitForElementByCss('#link').text()).toBe('to slug')
// Navigate to new page
await browser.elementByCss('#link').click()
expect(await browser.waitForElementByCss('#pathname').text()).toBe(
'/object/slug'
)
expect(await browser.elementByCss('#slug').text()).toBe('1')
})
it('should error when using dynamic href in app dir', async () => {
const browser = await next.browser('/string')
// Error should show up
expect(await hasRedbox(browser)).toBeTrue()
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"Error: Dynamic href \`/object/[slug]\` found in <Link> while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"`
)
})
} else {
it('should not error on /object in prod', async () => {
const browser = await next.browser('/object')
expect(await browser.elementByCss('#link').text()).toBe('to slug')
})
it('should not error on /string in prod', async () => {
const browser = await next.browser('/string')
expect(await browser.elementByCss('#link').text()).toBe('to slug')
})
}
})

View file

@ -1,43 +1,39 @@
import { type NextInstance, createNextDescribe } from 'e2e-utils'
import { type NextInstance, nextTestSetup } from 'e2e-utils'
async function getLastModifiedTime(next: NextInstance, pathname: string) {
const content = await (await next.fetch(pathname)).text()
return content.match(/<lastmod>([^<]+)<\/lastmod>/)[1]
}
createNextDescribe(
'app-dir - dynamic in generate params',
{
describe('app-dir - dynamic in generate params', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
},
({ next, isNextDev }) => {
it('should render sitemap with generateSitemaps in force-dynamic config dynamically', async () => {
const firstTime = await getLastModifiedTime(
next,
isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'
)
const secondTime = await getLastModifiedTime(
next,
isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'
)
})
expect(firstTime).not.toEqual(secondTime)
})
it('should render sitemap with generateSitemaps in force-dynamic config dynamically', async () => {
const firstTime = await getLastModifiedTime(
next,
isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'
)
const secondTime = await getLastModifiedTime(
next,
isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'
)
it('should be able to call while generating multiple dynamic sitemaps', async () => {
expect(
(await next.fetch(isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'))
.status
).toBe(200)
expect(
(await next.fetch(isNextDev ? 'sitemap.xml/1' : '/sitemap/1.xml'))
.status
).toBe(200)
})
expect(firstTime).not.toEqual(secondTime)
})
it('should be able to call fetch while generating multiple dynamic pages', async () => {
expect((await next.fetch('/dynamic/0')).status).toBe(200)
expect((await next.fetch('/dynamic/1')).status).toBe(200)
})
}
)
it('should be able to call while generating multiple dynamic sitemaps', async () => {
expect(
(await next.fetch(isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml')).status
).toBe(200)
expect(
(await next.fetch(isNextDev ? 'sitemap.xml/1' : '/sitemap/1.xml')).status
).toBe(200)
})
it('should be able to call fetch while generating multiple dynamic pages', async () => {
expect((await next.fetch('/dynamic/0')).status).toBe(200)
expect((await next.fetch('/dynamic/1')).status).toBe(200)
})
})

View file

@ -1,126 +1,122 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - next/dynamic',
{
describe('app dir - next/dynamic', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextStart }) => {
it('should handle ssr: false in pages when appDir is enabled', async () => {
const $ = await next.render$('/legacy/no-ssr')
expect($.html()).not.toContain('navigator')
})
const browser = await next.browser('/legacy/no-ssr')
expect(
await browser.waitForElementByCss('#pure-client').text()
).toContain('navigator')
})
it('should handle next/dynamic in SSR correctly', async () => {
const $ = await next.render$('/dynamic')
// filter out the script
const selector = 'body div'
const serverContent = $(selector).text()
// should load chunks generated via async import correctly with React.lazy
expect(serverContent).toContain('next-dynamic lazy')
// should support `dynamic` in both server and client components
expect(serverContent).toContain('next-dynamic dynamic on server')
expect(serverContent).toContain('next-dynamic dynamic on client')
expect(serverContent).toContain('next-dynamic server import client')
expect(serverContent).not.toContain(
'next-dynamic dynamic no ssr on client'
)
expect(serverContent).not.toContain(
'next-dynamic dynamic no ssr on server'
)
// client component under server component with ssr: false will not be rendered either in flight or SSR
expect($.html()).not.toContain('client component under sever no ssr')
})
it('should handle next/dynamic in hydration correctly', async () => {
const selector = 'body div'
const browser = await next.browser('/dynamic')
const clientContent = await browser.elementByCss(selector).text()
expect(clientContent).toContain('next-dynamic dynamic no ssr on server')
expect(clientContent).toContain('client component under sever no ssr')
await browser.waitForElementByCss('#css-text-dynamic-no-ssr-client')
expect(
await browser.elementByCss('#css-text-dynamic-no-ssr-client').text()
).toBe('next-dynamic dynamic no ssr on client:suffix')
})
it('should generate correct client manifest for dynamic chunks', async () => {
const $ = await next.render$('/chunk-loading/server')
expect($('h1').text()).toBe('hello')
})
describe('no SSR', () => {
it('should not render client component imported through ssr: false in client components in edge runtime', async () => {
// noSSR should not show up in html
const $ = await next.render$('/dynamic-mixed-ssr-false/client-edge')
expect($('#server-false-server-module')).not.toContain(
'ssr-false-server-module-text'
)
expect($('#server-false-client-module')).not.toContain(
'ssr-false-client-module-text'
)
// noSSR should not show up in browser
const browser = await next.browser(
'/dynamic-mixed-ssr-false/client-edge'
)
expect(
await browser.elementByCss('#ssr-false-server-module').text()
).toBe('ssr-false-server-module-text')
expect(
await browser.elementByCss('#ssr-false-client-module').text()
).toBe('ssr-false-client-module-text')
// in the server bundle should not contain client component imported through ssr: false
if (isNextStart) {
const chunkPath =
'.next/server/app/dynamic-mixed-ssr-false/client-edge/page.js'
const edgeServerChunk = await next.readFile(chunkPath)
expect(edgeServerChunk).not.toContain('ssr-false-client-module-text')
}
})
it('should not render client component imported through ssr: false in client components', async () => {
// noSSR should not show up in html
const $ = await next.render$('/dynamic-mixed-ssr-false/client')
expect($('#client-false-server-module')).not.toContain(
'ssr-false-server-module-text'
)
expect($('#client-false-client-module')).not.toContain(
'ssr-false-client-module-text'
)
// noSSR should not show up in browser
const browser = await next.browser('/dynamic-mixed-ssr-false/client')
expect(
await browser.elementByCss('#ssr-false-server-module').text()
).toBe('ssr-false-server-module-text')
expect(
await browser.elementByCss('#ssr-false-client-module').text()
).toBe('ssr-false-client-module-text')
// in the server bundle should not contain both server and client component imported through ssr: false
if (isNextStart) {
const pageServerChunk = await next.readFile(
'.next/server/app/dynamic-mixed-ssr-false/client/page.js'
)
expect(pageServerChunk).not.toContain('ssr-false-server-module-text')
expect(pageServerChunk).not.toContain('ssr-false-client-module-text')
}
})
it('should support dynamic import with accessing named exports from client component', async () => {
const $ = await next.render$('/dynamic/named-export')
expect($('#client-button').text()).toBe('this is a client button')
})
})
if (skipped) {
return
}
)
it('should handle ssr: false in pages when appDir is enabled', async () => {
const $ = await next.render$('/legacy/no-ssr')
expect($.html()).not.toContain('navigator')
const browser = await next.browser('/legacy/no-ssr')
expect(await browser.waitForElementByCss('#pure-client').text()).toContain(
'navigator'
)
})
it('should handle next/dynamic in SSR correctly', async () => {
const $ = await next.render$('/dynamic')
// filter out the script
const selector = 'body div'
const serverContent = $(selector).text()
// should load chunks generated via async import correctly with React.lazy
expect(serverContent).toContain('next-dynamic lazy')
// should support `dynamic` in both server and client components
expect(serverContent).toContain('next-dynamic dynamic on server')
expect(serverContent).toContain('next-dynamic dynamic on client')
expect(serverContent).toContain('next-dynamic server import client')
expect(serverContent).not.toContain('next-dynamic dynamic no ssr on client')
expect(serverContent).not.toContain('next-dynamic dynamic no ssr on server')
// client component under server component with ssr: false will not be rendered either in flight or SSR
expect($.html()).not.toContain('client component under sever no ssr')
})
it('should handle next/dynamic in hydration correctly', async () => {
const selector = 'body div'
const browser = await next.browser('/dynamic')
const clientContent = await browser.elementByCss(selector).text()
expect(clientContent).toContain('next-dynamic dynamic no ssr on server')
expect(clientContent).toContain('client component under sever no ssr')
await browser.waitForElementByCss('#css-text-dynamic-no-ssr-client')
expect(
await browser.elementByCss('#css-text-dynamic-no-ssr-client').text()
).toBe('next-dynamic dynamic no ssr on client:suffix')
})
it('should generate correct client manifest for dynamic chunks', async () => {
const $ = await next.render$('/chunk-loading/server')
expect($('h1').text()).toBe('hello')
})
describe('no SSR', () => {
it('should not render client component imported through ssr: false in client components in edge runtime', async () => {
// noSSR should not show up in html
const $ = await next.render$('/dynamic-mixed-ssr-false/client-edge')
expect($('#server-false-server-module')).not.toContain(
'ssr-false-server-module-text'
)
expect($('#server-false-client-module')).not.toContain(
'ssr-false-client-module-text'
)
// noSSR should not show up in browser
const browser = await next.browser('/dynamic-mixed-ssr-false/client-edge')
expect(
await browser.elementByCss('#ssr-false-server-module').text()
).toBe('ssr-false-server-module-text')
expect(
await browser.elementByCss('#ssr-false-client-module').text()
).toBe('ssr-false-client-module-text')
// in the server bundle should not contain client component imported through ssr: false
if (isNextStart) {
const chunkPath =
'.next/server/app/dynamic-mixed-ssr-false/client-edge/page.js'
const edgeServerChunk = await next.readFile(chunkPath)
expect(edgeServerChunk).not.toContain('ssr-false-client-module-text')
}
})
it('should not render client component imported through ssr: false in client components', async () => {
// noSSR should not show up in html
const $ = await next.render$('/dynamic-mixed-ssr-false/client')
expect($('#client-false-server-module')).not.toContain(
'ssr-false-server-module-text'
)
expect($('#client-false-client-module')).not.toContain(
'ssr-false-client-module-text'
)
// noSSR should not show up in browser
const browser = await next.browser('/dynamic-mixed-ssr-false/client')
expect(
await browser.elementByCss('#ssr-false-server-module').text()
).toBe('ssr-false-server-module-text')
expect(
await browser.elementByCss('#ssr-false-client-module').text()
).toBe('ssr-false-client-module-text')
// in the server bundle should not contain both server and client component imported through ssr: false
if (isNextStart) {
const pageServerChunk = await next.readFile(
'.next/server/app/dynamic-mixed-ssr-false/client/page.js'
)
expect(pageServerChunk).not.toContain('ssr-false-server-module-text')
expect(pageServerChunk).not.toContain('ssr-false-client-module-text')
}
})
it('should support dynamic import with accessing named exports from client component', async () => {
const $ = await next.render$('/dynamic/named-export')
expect($('#client-button').text()).toBe('this is a client button')
})
})
})

View file

@ -1,23 +1,21 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'edge-route-catchall',
{
describe('edge-route-catchall', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should correctly normalize edge route catch-all with a single param', async () => {
const result = await next.fetch('/edge/one')
})
expect(await result.text()).toBe(JSON.stringify({ slug: ['one'] }))
})
it('should correctly normalize edge route catch-all with a single param', async () => {
const result = await next.fetch('/edge/one')
it('should correctly normalize edge route catch-all with multiple params', async () => {
const result = await next.fetch('/edge/one/two/three')
expect(await result.text()).toBe(JSON.stringify({ slug: ['one'] }))
})
expect(await result.text()).toBe(
JSON.stringify({ slug: ['one', 'two', 'three'] })
)
})
}
)
it('should correctly normalize edge route catch-all with multiple params', async () => {
const result = await next.fetch('/edge/one/two/three')
expect(await result.text()).toBe(
JSON.stringify({ slug: ['one', 'two', 'three'] })
)
})
})

View file

@ -1,21 +1,19 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'edge-route-rewrite',
{
describe('edge-route-rewrite', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('it should support a rewrite to an edge route', async () => {
const result = await next.render('/one/example')
expect(result).toContain('Hello from /app/two/example/route.ts')
})
})
it('it should support a rewrite to a dynamic edge route', async () => {
const result = await next.render('/dynamic-test/foo')
expect(result).toContain(
'Hello from /app/dynamic/[slug]/route.ts. Slug: foo'
)
})
}
)
it('it should support a rewrite to an edge route', async () => {
const result = await next.render('/one/example')
expect(result).toContain('Hello from /app/two/example/route.ts')
})
it('it should support a rewrite to a dynamic edge route', async () => {
const result = await next.render('/dynamic-test/foo')
expect(result).toContain(
'Hello from /app/dynamic/[slug]/route.ts. Slug: foo'
)
})
})

View file

@ -1,51 +1,49 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'edge runtime node compatibility',
{
describe('edge runtime node compatibility', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('[app] supports node:buffer', async () => {
const res = await next.fetch('/buffer', {
method: 'POST',
body: 'Hello, world!',
})
const json = await res.json()
expect(json).toEqual({
'Buffer === B.Buffer': true,
encoded: Buffer.from('Hello, world!').toString('base64'),
exposedKeys: expect.arrayContaining([
'constants',
'kMaxLength',
'kStringMaxLength',
'Buffer',
'SlowBuffer',
]),
})
})
})
it('[pages/api] supports node:buffer', async () => {
const res = await next.fetch('/api/buffer', {
method: 'POST',
body: 'Hello, world!',
})
const json = await res.json()
expect(json).toEqual({
'B2.Buffer === B.Buffer': true,
'Buffer === B.Buffer': true,
'typeof B.Buffer': 'function',
'typeof B2.Buffer': 'function',
'typeof Buffer': 'function',
encoded: 'SGVsbG8sIHdvcmxkIQ==',
exposedKeys: expect.arrayContaining([
'constants',
'kMaxLength',
'kStringMaxLength',
'Buffer',
'SlowBuffer',
]),
})
it('[app] supports node:buffer', async () => {
const res = await next.fetch('/buffer', {
method: 'POST',
body: 'Hello, world!',
})
}
)
const json = await res.json()
expect(json).toEqual({
'Buffer === B.Buffer': true,
encoded: Buffer.from('Hello, world!').toString('base64'),
exposedKeys: expect.arrayContaining([
'constants',
'kMaxLength',
'kStringMaxLength',
'Buffer',
'SlowBuffer',
]),
})
})
it('[pages/api] supports node:buffer', async () => {
const res = await next.fetch('/api/buffer', {
method: 'POST',
body: 'Hello, world!',
})
const json = await res.json()
expect(json).toEqual({
'B2.Buffer === B.Buffer': true,
'Buffer === B.Buffer': true,
'typeof B.Buffer': 'function',
'typeof B2.Buffer': 'function',
'typeof Buffer': 'function',
encoded: 'SGVsbG8sIHdvcmxkIQ==',
exposedKeys: expect.arrayContaining([
'constants',
'kMaxLength',
'kStringMaxLength',
'Buffer',
'SlowBuffer',
]),
})
})
})

View file

@ -1,28 +1,30 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'app dir - emotion-js',
{
describe('app dir - emotion-js', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
dependencies: {
'@emotion/react': 'latest',
'@emotion/cache': 'latest',
},
},
({ next }) => {
it('should render emotion-js css with compiler.emotion option correctly', async () => {
const browser = await next.browser('/')
const el = browser.elementByCss('h1')
expect(await el.text()).toBe('Blue')
await check(
async () =>
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
),
'rgb(0, 0, 255)'
)
})
})
if (skipped) {
return
}
)
it('should render emotion-js css with compiler.emotion option correctly', async () => {
const browser = await next.browser('/')
const el = browser.elementByCss('h1')
expect(await el.text()).toBe('Blue')
await check(
async () =>
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
),
'rgb(0, 0, 255)'
)
})
})

View file

@ -1,4 +1,4 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
export function runTest({ next }) {
it('should allow navigation on not-found', async () => {
@ -125,12 +125,10 @@ export function runTest({ next }) {
})
}
createNextDescribe(
'app dir - not found navigation',
{
describe('app dir - not found navigation', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
runTest({ next })
}
)
})
runTest({ next })
})

View file

@ -1,13 +1,11 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { runTest } from './index.test'
createNextDescribe(
'app dir - not found navigation - with overridden node env',
{
describe('app dir - not found navigation - with overridden node env', () => {
const { next } = nextTestSetup({
files: __dirname,
env: { NODE_ENV: 'test' },
},
({ next }) => {
runTest({ next })
}
)
})
runTest({ next })
})

View file

@ -1,176 +1,178 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { getRedboxHeader, hasRedbox, retry } from 'next-test-utils'
createNextDescribe(
'app-dir - errors',
{
describe('app-dir - errors', () => {
const { next, isNextDev, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextDev, isNextStart }) => {
describe('error component', () => {
it('should trigger error component when an error happens during rendering', async () => {
const browser = await next.browser('/client-component')
await browser.elementByCss('#error-trigger-button').click()
})
if (isNextDev) {
// TODO: investigate desired behavior here as it is currently
// minimized by default
// expect(await hasRedbox(browser)).toBe(true)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
if (skipped) {
return
}
describe('error component', () => {
it('should trigger error component when an error happens during rendering', async () => {
const browser = await next.browser('/client-component')
await browser.elementByCss('#error-trigger-button').click()
if (isNextDev) {
// TODO: investigate desired behavior here as it is currently
// minimized by default
// expect(await hasRedbox(browser)).toBe(true)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
await browser
expect(
await browser
expect(
await browser
.waitForElementByCss('#error-boundary-message')
.elementByCss('#error-boundary-message')
.text()
).toBe('An error occurred: this is a test')
}
})
it('should trigger error component when an error happens during server components rendering', async () => {
const browser = await next.browser('/server-component')
if (isNextDev) {
expect(
await browser
.waitForElementByCss('#error-boundary-message')
.elementByCss('#error-boundary-message')
.text()
).toBe('this is a test')
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
// TODO-APP: ensure error overlay is shown for errors that happened before/during hydration
// expect(await hasRedbox(browser)).toBe(true)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
await browser
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
}
})
it('should use default error boundary for prod and overlay for dev when no error component specified', async () => {
const browser = await next.browser('/global-error-boundary/client')
await browser.elementByCss('#error-trigger-button').click()
if (isNextDev) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h2').text()
).toBe(
'Application error: a client-side exception has occurred (see the browser console for more information).'
)
}
})
it('should display error digest for error in server component with default error boundary', async () => {
const browser = await next.browser('/global-error-boundary/server')
if (isNextDev) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(/custom server error/)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h2').text()
).toBe(
'Application error: a server-side exception has occurred (see the server logs for more information).'
)
expect(
await browser.waitForElementByCss('body').elementByCss('p').text()
).toMatch(/Digest: \w+/)
}
})
// production tests
if (isNextStart) {
it('should allow resetting error boundary', async () => {
const browser = await next.browser('/client-component')
// Try triggering and resetting a few times in a row
for (let i = 0; i < 5; i++) {
await browser
.elementByCss('#error-trigger-button')
.click()
.waitForElementByCss('#error-boundary-message')
expect(
await browser.elementByCss('#error-boundary-message').text()
).toBe('An error occurred: this is a test')
await browser
.elementByCss('#reset')
.click()
.waitForElementByCss('#error-trigger-button')
expect(
await browser.elementByCss('#error-trigger-button').text()
).toBe('Trigger Error!')
}
})
it('should hydrate empty shell to handle server-side rendering errors', async () => {
const browser = await next.browser('/ssr-error-client-component')
const logs = await browser.log()
const errors = logs
.filter((x) => x.source === 'error')
.map((x) => x.message)
.join('\n')
expect(errors).toInclude('Error during SSR')
})
it('should log the original RSC error trace in production', async () => {
const logIndex = next.cliOutput.length
const browser = await next.browser('/server-component')
const digest = await browser
.waitForElementByCss('#error-boundary-digest')
.waitForElementByCss('#error-boundary-message')
.elementByCss('#error-boundary-message')
.text()
const output = next.cliOutput.slice(logIndex)
// Log the original rsc error trace
expect(output).toContain('Error: this is a test')
// Does not include the react renderer error for server actions
expect(output).not.toContain(
'Error: An error occurred in the Server Components render'
)
expect(output).toContain(`digest: '${digest}'`)
})
it('should log the original Server Actions error trace in production', async () => {
const logIndex = next.cliOutput.length
const browser = await next.browser('/server-actions')
// trigger server action
await browser.elementByCss('#button').click()
// wait for response
let digest
await retry(async () => {
digest = await browser.waitForElementByCss('#digest').text()
expect(digest).toMatch(/\d+/)
})
const output = next.cliOutput.slice(logIndex)
// Log the original rsc error trace
expect(output).toContain('Error: server action test error')
// Does not include the react renderer error for server actions
expect(output).not.toContain(
'Error: An error occurred in the Server Components render'
)
expect(output).toContain(`digest: '${digest}'`)
})
).toBe('An error occurred: this is a test')
}
})
}
)
it('should trigger error component when an error happens during server components rendering', async () => {
const browser = await next.browser('/server-component')
if (isNextDev) {
expect(
await browser
.waitForElementByCss('#error-boundary-message')
.elementByCss('#error-boundary-message')
.text()
).toBe('this is a test')
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
// TODO-APP: ensure error overlay is shown for errors that happened before/during hydration
// expect(await hasRedbox(browser)).toBe(true)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
await browser
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
}
})
it('should use default error boundary for prod and overlay for dev when no error component specified', async () => {
const browser = await next.browser('/global-error-boundary/client')
await browser.elementByCss('#error-trigger-button').click()
if (isNextDev) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h2').text()
).toBe(
'Application error: a client-side exception has occurred (see the browser console for more information).'
)
}
})
it('should display error digest for error in server component with default error boundary', async () => {
const browser = await next.browser('/global-error-boundary/server')
if (isNextDev) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(/custom server error/)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h2').text()
).toBe(
'Application error: a server-side exception has occurred (see the server logs for more information).'
)
expect(
await browser.waitForElementByCss('body').elementByCss('p').text()
).toMatch(/Digest: \w+/)
}
})
// production tests
if (isNextStart) {
it('should allow resetting error boundary', async () => {
const browser = await next.browser('/client-component')
// Try triggering and resetting a few times in a row
for (let i = 0; i < 5; i++) {
await browser
.elementByCss('#error-trigger-button')
.click()
.waitForElementByCss('#error-boundary-message')
expect(
await browser.elementByCss('#error-boundary-message').text()
).toBe('An error occurred: this is a test')
await browser
.elementByCss('#reset')
.click()
.waitForElementByCss('#error-trigger-button')
expect(
await browser.elementByCss('#error-trigger-button').text()
).toBe('Trigger Error!')
}
})
it('should hydrate empty shell to handle server-side rendering errors', async () => {
const browser = await next.browser('/ssr-error-client-component')
const logs = await browser.log()
const errors = logs
.filter((x) => x.source === 'error')
.map((x) => x.message)
.join('\n')
expect(errors).toInclude('Error during SSR')
})
it('should log the original RSC error trace in production', async () => {
const logIndex = next.cliOutput.length
const browser = await next.browser('/server-component')
const digest = await browser
.waitForElementByCss('#error-boundary-digest')
.text()
const output = next.cliOutput.slice(logIndex)
// Log the original rsc error trace
expect(output).toContain('Error: this is a test')
// Does not include the react renderer error for server actions
expect(output).not.toContain(
'Error: An error occurred in the Server Components render'
)
expect(output).toContain(`digest: '${digest}'`)
})
it('should log the original Server Actions error trace in production', async () => {
const logIndex = next.cliOutput.length
const browser = await next.browser('/server-actions')
// trigger server action
await browser.elementByCss('#button').click()
// wait for response
let digest
await retry(async () => {
digest = await browser.waitForElementByCss('#digest').text()
expect(digest).toMatch(/\d+/)
})
const output = next.cliOutput.slice(logIndex)
// Log the original rsc error trace
expect(output).toContain('Error: server action test error')
// Does not include the react renderer error for server actions
expect(output).not.toContain(
'Error: An error occurred in the Server Components render'
)
expect(output).toContain(`digest: '${digest}'`)
})
}
})
})

View file

@ -1,20 +1,15 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - front redirect issue',
{
describe('app dir - front redirect issue', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should redirect', async () => {
const browser = await next.browser('/vercel-user')
expect(
await browser
.waitForElementByCss('#home-page')
.elementByCss('h1')
.text()
).toBe('Hello!')
expect(await browser.url()).toBe(`${next.url}/vercel-user`)
})
}
)
})
it('should redirect', async () => {
const browser = await next.browser('/vercel-user')
expect(
await browser.waitForElementByCss('#home-page').elementByCss('h1').text()
).toBe('Hello!')
expect(await browser.url()).toBe(`${next.url}/vercel-user`)
})
})

View file

@ -1,95 +1,93 @@
import { getRedboxHeader, hasRedbox } from 'next-test-utils'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
async function testDev(browser, errorRegex) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(errorRegex)
}
createNextDescribe(
'app dir - global error',
{
describe('app dir - global error', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
},
({ next, isNextDev }) => {
it('should trigger error component when an error happens during rendering', async () => {
const browser = await next.browser('/client')
})
it('should trigger error component when an error happens during rendering', async () => {
const browser = await next.browser('/client')
await browser
.waitForElementByCss('#error-trigger-button')
.elementByCss('#error-trigger-button')
.click()
if (isNextDev) {
await testDev(browser, /Error: Client error/)
} else {
await browser
.waitForElementByCss('#error-trigger-button')
.elementByCss('#error-trigger-button')
.click()
if (isNextDev) {
await testDev(browser, /Error: Client error/)
} else {
await browser
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: Client error'
)
}
})
it('should render global error for error in server components', async () => {
const browser = await next.browser('/ssr/server')
if (isNextDev) {
await testDev(browser, /Error: server page error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(await browser.elementByCss('#digest').text()).toMatch(/\w+/)
}
})
it('should render global error for error in client components', async () => {
const browser = await next.browser('/ssr/client')
if (isNextDev) {
await testDev(browser, /Error: client page error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: client page error'
)
expect(await browser.hasElementByCssSelector('#digest')).toBeFalsy()
}
})
it('should catch metadata error in error boundary if presented', async () => {
const browser = await next.browser('/metadata-error-with-boundary')
expect(await browser.elementByCss('#error').text()).toBe(
'Local error boundary'
'Global error: Client error'
)
}
})
it('should render global error for error in server components', async () => {
const browser = await next.browser('/ssr/server')
if (isNextDev) {
await testDev(browser, /Error: server page error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(await browser.elementByCss('#digest').text()).toMatch(/\w+/)
}
})
it('should render global error for error in client components', async () => {
const browser = await next.browser('/ssr/client')
if (isNextDev) {
await testDev(browser, /Error: client page error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: client page error'
)
expect(await browser.hasElementByCssSelector('#digest')).toBeFalsy()
})
}
})
it('should catch metadata error in global-error if no error boundary is presented', async () => {
const browser = await next.browser('/metadata-error-without-boundary')
it('should catch metadata error in error boundary if presented', async () => {
const browser = await next.browser('/metadata-error-with-boundary')
if (isNextDev) {
await testDev(browser, /Error: Metadata error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
}
})
expect(await browser.elementByCss('#error').text()).toBe(
'Local error boundary'
)
expect(await browser.hasElementByCssSelector('#digest')).toBeFalsy()
})
it('should catch the client error thrown in the nested routes', async () => {
const browser = await next.browser('/nested/nested')
if (isNextDev) {
await testDev(browser, /Error: nested error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: nested error'
)
}
})
}
)
it('should catch metadata error in global-error if no error boundary is presented', async () => {
const browser = await next.browser('/metadata-error-without-boundary')
if (isNextDev) {
await testDev(browser, /Error: Metadata error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
}
})
it('should catch the client error thrown in the nested routes', async () => {
const browser = await next.browser('/nested/nested')
if (isNextDev) {
await testDev(browser, /Error: nested error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: nested error'
)
}
})
})

View file

@ -1,29 +1,29 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - global error - with catch-all route',
{
describe('app dir - global error - with catch-all route', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextStart }) => {
it('should render catch-all route correctly', async () => {
expect(await next.render('/en/foo')).toContain('catch-all page')
})
})
it('should render 404 page correctly', async () => {
expect(await next.render('/en')).toContain(
'This page could not be found.'
)
})
if (isNextStart) {
it('should render global error correctly', async () => {
const browser = await next.browser('/en/error')
const text = await browser.elementByCss('#global-error').text()
expect(text).toBe('global-error')
})
}
if (skipped) {
return
}
)
it('should render catch-all route correctly', async () => {
expect(await next.render('/en/foo')).toContain('catch-all page')
})
it('should render 404 page correctly', async () => {
expect(await next.render('/en')).toContain('This page could not be found.')
})
if (isNextStart) {
it('should render global error correctly', async () => {
const browser = await next.browser('/en/error')
const text = await browser.elementByCss('#global-error').text()
expect(text).toBe('global-error')
})
}
})

View file

@ -1,30 +1,32 @@
import { getRedboxHeader, hasRedbox } from 'next-test-utils'
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
async function testDev(browser, errorRegex) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(errorRegex)
}
createNextDescribe(
'app dir - global error - layout error',
{
describe('app dir - global error - layout error', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
},
({ next, isNextDev }) => {
it('should render global error for error in server components', async () => {
const browser = await next.browser('/')
})
if (isNextDev) {
await testDev(browser, /Global error: layout error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(await browser.elementByCss('#digest').text()).toMatch(/\w+/)
}
})
if (skipped) {
return
}
)
it('should render global error for error in server components', async () => {
const browser = await next.browser('/')
if (isNextDev) {
await testDev(browser, /Global error: layout error/)
} else {
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
'Global error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(await browser.elementByCss('#digest').text()).toMatch(/\w+/)
}
})
})

View file

@ -1,71 +1,69 @@
import { createNextDescribe } from '../../../lib/e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { outdent } from 'outdent'
createNextDescribe(
'headers-static-bailout',
{
describe('headers-static-bailout', () => {
const { next, isNextStart } = nextTestSetup({
files: __dirname,
dependencies: {
nanoid: '4.0.1',
},
},
({ next, isNextStart }) => {
if (!isNextStart) {
it('should skip', () => {})
return
}
})
it('should bailout when using an import from next/headers', async () => {
const url = '/page-with-headers'
const $ = await next.render$(url)
expect($('h1').text()).toBe('Dynamic Page')
// Check if the page is not statically generated.
const id = $('#nanoid').text()
const $2 = await next.render$(url)
const id2 = $2('#nanoid').text()
expect(id).not.toBe(id2)
})
it('should not bailout when not using headers', async () => {
const url = '/page-without-headers'
const $ = await next.render$(url)
expect($('h1').text()).toBe('Static Page')
// Check if the page is not statically generated.
const id = $('#nanoid').text()
const $2 = await next.render$(url)
const id2 = $2('#nanoid').text()
expect(id).toBe(id2)
})
it('it provides a helpful link in case static generation bailout is uncaught', async () => {
await next.stop()
await next.patchFile(
'app/server-components-page/page.tsx',
outdent`
import { cookies } from 'next/headers'
async function foo() {
return new Promise((resolve) =>
// break out of the expected async context, causing an uncaught build-time error
setTimeout(() => {
resolve(cookies().getAll())
}, 1000)
)
}
export default async function Page() {
await foo()
return <div>Hello World</div>
}
`
)
const { cliOutput } = await next.build()
expect(cliOutput).toContain(
'https://nextjs.org/docs/messages/dynamic-server-error'
)
})
if (!isNextStart) {
it('should skip', () => {})
return
}
)
it('should bailout when using an import from next/headers', async () => {
const url = '/page-with-headers'
const $ = await next.render$(url)
expect($('h1').text()).toBe('Dynamic Page')
// Check if the page is not statically generated.
const id = $('#nanoid').text()
const $2 = await next.render$(url)
const id2 = $2('#nanoid').text()
expect(id).not.toBe(id2)
})
it('should not bailout when not using headers', async () => {
const url = '/page-without-headers'
const $ = await next.render$(url)
expect($('h1').text()).toBe('Static Page')
// Check if the page is not statically generated.
const id = $('#nanoid').text()
const $2 = await next.render$(url)
const id2 = $2('#nanoid').text()
expect(id).toBe(id2)
})
it('it provides a helpful link in case static generation bailout is uncaught', async () => {
await next.stop()
await next.patchFile(
'app/server-components-page/page.tsx',
outdent`
import { cookies } from 'next/headers'
async function foo() {
return new Promise((resolve) =>
// break out of the expected async context, causing an uncaught build-time error
setTimeout(() => {
resolve(cookies().getAll())
}, 1000)
)
}
export default async function Page() {
await foo()
return <div>Hello World</div>
}
`
)
const { cliOutput } = await next.build()
expect(cliOutput).toContain(
'https://nextjs.org/docs/messages/dynamic-server-error'
)
})
})

View file

@ -1,34 +1,32 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'hello-world',
{
describe('hello-world', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
it('should work using cheerio', async () => {
const $ = await next.render$('/')
expect($('p').text()).toBe('hello world')
})
})
// Recommended for tests that need a full browser
it('should work using browser', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('hello world')
})
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
it('should work using cheerio', async () => {
const $ = await next.render$('/')
expect($('p').text()).toBe('hello world')
})
// In case you need the full HTML. Can also use $.html() with cheerio.
it('should work with html', async () => {
const html = await next.render('/')
expect(html).toContain('hello world')
})
// Recommended for tests that need a full browser
it('should work using browser', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('hello world')
})
// In case you need to test the response object
it('should work with fetch', async () => {
const res = await next.fetch('/')
const html = await res.text()
expect(html).toContain('hello world')
})
}
)
// In case you need the full HTML. Can also use $.html() with cheerio.
it('should work with html', async () => {
const html = await next.render('/')
expect(html).toContain('hello world')
})
// In case you need to test the response object
it('should work with fetch', async () => {
const res = await next.fetch('/')
const html = await res.text()
expect(html).toContain('hello world')
})
})

View file

@ -1,186 +1,184 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - hooks',
{
describe('app dir - hooks', () => {
const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
},
({ next, isNextDeploy }) => {
describe('from pages', () => {
it.each([
{ pathname: '/adapter-hooks/static' },
{ pathname: '/adapter-hooks/1' },
{ pathname: '/adapter-hooks/2' },
{ pathname: '/adapter-hooks/1/account' },
{ pathname: '/adapter-hooks/static', keyValue: 'value' },
{ pathname: '/adapter-hooks/1', keyValue: 'value' },
{ pathname: '/adapter-hooks/2', keyValue: 'value' },
{ pathname: '/adapter-hooks/1/account', keyValue: 'value' },
])(
'should have the correct hooks at $pathname',
async ({ pathname, keyValue = '' }) => {
const browser = await next.browser(
pathname + (keyValue ? `?key=${keyValue}` : '')
)
})
try {
await browser.waitForElementByCss('#router-ready')
expect(await browser.elementById('key-value').text()).toBe(keyValue)
expect(await browser.elementById('pathname').text()).toBe(pathname)
await browser.elementByCss('button').click()
await browser.waitForElementByCss('#pushed')
} finally {
await browser.close()
}
}
)
})
describe('usePathname', () => {
it('should have the correct pathname', async () => {
const $ = await next.render$('/hooks/use-pathname')
expect($('#pathname').attr('data-pathname')).toBe('/hooks/use-pathname')
})
it('should have the canonical url pathname on rewrite', async () => {
const $ = await next.render$('/rewritten-use-pathname')
expect($('#pathname').attr('data-pathname')).toBe(
'/rewritten-use-pathname'
describe('from pages', () => {
it.each([
{ pathname: '/adapter-hooks/static' },
{ pathname: '/adapter-hooks/1' },
{ pathname: '/adapter-hooks/2' },
{ pathname: '/adapter-hooks/1/account' },
{ pathname: '/adapter-hooks/static', keyValue: 'value' },
{ pathname: '/adapter-hooks/1', keyValue: 'value' },
{ pathname: '/adapter-hooks/2', keyValue: 'value' },
{ pathname: '/adapter-hooks/1/account', keyValue: 'value' },
])(
'should have the correct hooks at $pathname',
async ({ pathname, keyValue = '' }) => {
const browser = await next.browser(
pathname + (keyValue ? `?key=${keyValue}` : '')
)
})
})
describe('useSearchParams', () => {
it('should have the correct search params', async () => {
const $ = await next.render$(
'/hooks/use-search-params?first=value&second=other%20value&third'
)
expect($('#params-first').text()).toBe('value')
expect($('#params-second').text()).toBe('other value')
expect($('#params-third').text()).toBe('')
expect($('#params-not-real').text()).toBe('N/A')
})
// TODO-APP: correct this behavior when deployed
if (!isNextDeploy) {
it('should have the canonical url search params on rewrite', async () => {
const $ = await next.render$(
'/rewritten-use-search-params?first=a&second=b&third=c'
)
expect($('#params-first').text()).toBe('a')
expect($('#params-second').text()).toBe('b')
expect($('#params-third').text()).toBe('c')
expect($('#params-not-real').text()).toBe('N/A')
})
}
})
describe('useDraftMode', () => {
let initialRand = 'unintialized'
it('should use initial rand when draft mode be disabled', async () => {
const $ = await next.render$('/hooks/use-draft-mode')
expect($('#draft-mode-val').text()).toBe('DISABLED')
expect($('#rand').text()).toBeDefined()
initialRand = $('#rand').text()
})
it('should generate rand when draft mode enabled', async () => {
const res = await next.fetch('/enable')
const h = res.headers.get('set-cookie') || ''
const cookie = h
.split(';')
.find((c) => c.startsWith('__prerender_bypass'))
const $ = await next.render$(
'/hooks/use-draft-mode',
{},
{
headers: {
Cookie: cookie,
},
}
)
expect($('#draft-mode-val').text()).toBe('ENABLED')
expect($('#rand').text()).not.toBe(initialRand)
})
})
describe('useRouter', () => {
it('should allow access to the router', async () => {
const browser = await next.browser('/hooks/use-router')
try {
// Wait for the page to load, click the button (which uses a method
// on the router) and then wait for the correct page to load.
await browser.waitForElementByCss('#router')
await browser.elementById('button-push').click()
await browser.waitForElementByCss('#router-sub-page')
await browser.waitForElementByCss('#router-ready')
expect(await browser.elementById('key-value').text()).toBe(keyValue)
expect(await browser.elementById('pathname').text()).toBe(pathname)
// Go back (confirming we did do a hard push), and wait for the
// correct previous page.
await browser.back()
await browser.waitForElementByCss('#router')
await browser.elementByCss('button').click()
await browser.waitForElementByCss('#pushed')
} finally {
await browser.close()
}
})
}
)
})
describe('usePathname', () => {
it('should have the correct pathname', async () => {
const $ = await next.render$('/hooks/use-pathname')
expect($('#pathname').attr('data-pathname')).toBe('/hooks/use-pathname')
})
describe('useSelectedLayoutSegments', () => {
it.each`
path | outerLayout | innerLayout
${'/hooks/use-selected-layout-segment/first'} | ${['first']} | ${[]}
${'/hooks/use-selected-layout-segment/first/slug1'} | ${['first', 'slug1']} | ${['slug1']}
${'/hooks/use-selected-layout-segment/first/slug2/second'} | ${['first', 'slug2', '(group)', 'second']} | ${['slug2', '(group)', 'second']}
${'/hooks/use-selected-layout-segment/first/slug2/second/a/b'} | ${['first', 'slug2', '(group)', 'second', 'a/b']} | ${['slug2', '(group)', 'second', 'a/b']}
${'/hooks/use-selected-layout-segment/rewritten'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']}
${'/hooks/use-selected-layout-segment/rewritten-middleware'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']}
`(
'should have the correct layout segments at $path',
async ({ path, outerLayout, innerLayout }) => {
const $ = await next.render$(path)
it('should have the canonical url pathname on rewrite', async () => {
const $ = await next.render$('/rewritten-use-pathname')
expect($('#pathname').attr('data-pathname')).toBe(
'/rewritten-use-pathname'
)
})
})
expect(JSON.parse($('#outer-layout').text())).toEqual(outerLayout)
expect(JSON.parse($('#inner-layout').text())).toEqual(innerLayout)
describe('useSearchParams', () => {
it('should have the correct search params', async () => {
const $ = await next.render$(
'/hooks/use-search-params?first=value&second=other%20value&third'
)
expect($('#params-first').text()).toBe('value')
expect($('#params-second').text()).toBe('other value')
expect($('#params-third').text()).toBe('')
expect($('#params-not-real').text()).toBe('N/A')
})
// TODO-APP: correct this behavior when deployed
if (!isNextDeploy) {
it('should have the canonical url search params on rewrite', async () => {
const $ = await next.render$(
'/rewritten-use-search-params?first=a&second=b&third=c'
)
expect($('#params-first').text()).toBe('a')
expect($('#params-second').text()).toBe('b')
expect($('#params-third').text()).toBe('c')
expect($('#params-not-real').text()).toBe('N/A')
})
}
})
describe('useDraftMode', () => {
let initialRand = 'unintialized'
it('should use initial rand when draft mode be disabled', async () => {
const $ = await next.render$('/hooks/use-draft-mode')
expect($('#draft-mode-val').text()).toBe('DISABLED')
expect($('#rand').text()).toBeDefined()
initialRand = $('#rand').text()
})
it('should generate rand when draft mode enabled', async () => {
const res = await next.fetch('/enable')
const h = res.headers.get('set-cookie') || ''
const cookie = h
.split(';')
.find((c) => c.startsWith('__prerender_bypass'))
const $ = await next.render$(
'/hooks/use-draft-mode',
{},
{
headers: {
Cookie: cookie,
},
}
)
it('should return an empty array in pages', async () => {
const $ = await next.render$(
'/hooks/use-selected-layout-segment/first/slug2/second/a/b'
)
expect(JSON.parse($('#page-layout-segments').text())).toEqual([])
})
expect($('#draft-mode-val').text()).toBe('ENABLED')
expect($('#rand').text()).not.toBe(initialRand)
})
})
describe('useSelectedLayoutSegment', () => {
it.each`
path | outerLayout | innerLayout
${'/hooks/use-selected-layout-segment/first'} | ${'first'} | ${null}
${'/hooks/use-selected-layout-segment/first/slug1'} | ${'first'} | ${'slug1'}
${'/hooks/use-selected-layout-segment/first/slug2/second/a/b'} | ${'first'} | ${'slug2'}
`(
'should have the correct layout segment at $path',
async ({ path, outerLayout, innerLayout }) => {
const $ = await next.render$(path)
describe('useRouter', () => {
it('should allow access to the router', async () => {
const browser = await next.browser('/hooks/use-router')
expect(JSON.parse($('#outer-layout-segment').text())).toEqual(
outerLayout
)
expect(JSON.parse($('#inner-layout-segment').text())).toEqual(
innerLayout
)
}
try {
// Wait for the page to load, click the button (which uses a method
// on the router) and then wait for the correct page to load.
await browser.waitForElementByCss('#router')
await browser.elementById('button-push').click()
await browser.waitForElementByCss('#router-sub-page')
// Go back (confirming we did do a hard push), and wait for the
// correct previous page.
await browser.back()
await browser.waitForElementByCss('#router')
} finally {
await browser.close()
}
})
})
describe('useSelectedLayoutSegments', () => {
it.each`
path | outerLayout | innerLayout
${'/hooks/use-selected-layout-segment/first'} | ${['first']} | ${[]}
${'/hooks/use-selected-layout-segment/first/slug1'} | ${['first', 'slug1']} | ${['slug1']}
${'/hooks/use-selected-layout-segment/first/slug2/second'} | ${['first', 'slug2', '(group)', 'second']} | ${['slug2', '(group)', 'second']}
${'/hooks/use-selected-layout-segment/first/slug2/second/a/b'} | ${['first', 'slug2', '(group)', 'second', 'a/b']} | ${['slug2', '(group)', 'second', 'a/b']}
${'/hooks/use-selected-layout-segment/rewritten'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']}
${'/hooks/use-selected-layout-segment/rewritten-middleware'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']}
`(
'should have the correct layout segments at $path',
async ({ path, outerLayout, innerLayout }) => {
const $ = await next.render$(path)
expect(JSON.parse($('#outer-layout').text())).toEqual(outerLayout)
expect(JSON.parse($('#inner-layout').text())).toEqual(innerLayout)
}
)
it('should return an empty array in pages', async () => {
const $ = await next.render$(
'/hooks/use-selected-layout-segment/first/slug2/second/a/b'
)
it('should return null in pages', async () => {
const $ = await next.render$(
'/hooks/use-selected-layout-segment/first/slug2/second/a/b'
)
expect(JSON.parse($('#page-layout-segment').text())).toEqual(null)
})
expect(JSON.parse($('#page-layout-segments').text())).toEqual([])
})
}
)
})
describe('useSelectedLayoutSegment', () => {
it.each`
path | outerLayout | innerLayout
${'/hooks/use-selected-layout-segment/first'} | ${'first'} | ${null}
${'/hooks/use-selected-layout-segment/first/slug1'} | ${'first'} | ${'slug1'}
${'/hooks/use-selected-layout-segment/first/slug2/second/a/b'} | ${'first'} | ${'slug2'}
`(
'should have the correct layout segment at $path',
async ({ path, outerLayout, innerLayout }) => {
const $ = await next.render$(path)
expect(JSON.parse($('#outer-layout-segment').text())).toEqual(
outerLayout
)
expect(JSON.parse($('#inner-layout-segment').text())).toEqual(
innerLayout
)
}
)
it('should return null in pages', async () => {
const $ = await next.render$(
'/hooks/use-selected-layout-segment/first/slug2/second/a/b'
)
expect(JSON.parse($('#page-layout-segment').text())).toEqual(null)
})
})
})

View file

@ -1,7 +1,7 @@
// @ts-check
// @ts-ignore
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import cheerio from 'cheerio'
const { i18n } = require('./next.config')
@ -47,36 +47,34 @@ const urls = [
})),
]
createNextDescribe(
'i18n-hybrid',
{
describe('i18n-hybrid', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it.each(urls.filter((url) => !url.expected))(
'does not resolve $pathname',
async (url) => {
const res = await next.fetch(url.pathname, {
redirect: 'manual',
})
})
expect(res.status).toBe(404)
}
)
it.each(urls.filter((url) => !url.expected))(
'does not resolve $pathname',
async (url) => {
const res = await next.fetch(url.pathname, {
redirect: 'manual',
})
it.each(urls.filter((url) => url.expected))(
'does resolve $pathname',
async (url) => {
const res = await next.fetch(url.pathname, {
redirect: 'manual',
})
expect(res.status).toBe(404)
}
)
expect(res.status).toBe(200)
it.each(urls.filter((url) => url.expected))(
'does resolve $pathname',
async (url) => {
const res = await next.fetch(url.pathname, {
redirect: 'manual',
})
const $ = cheerio.load(await res.text())
const debug = JSON.parse($('#debug').text())
expect(debug).toEqual(url.expected)
}
)
}
)
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
const debug = JSON.parse($('#debug').text())
expect(debug).toEqual(url.expected)
}
)
})

View file

@ -1,16 +1,14 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
createNextDescribe(
'app dir - imports',
{
describe('app dir - imports', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
;['js', 'jsx', 'ts', 'tsx'].forEach((ext) => {
it(`we can import all components from .${ext}`, async () => {
const $ = await next.render$(`/${ext}`)
expect($('#js').text()).toBe('CompJs')
})
})
;['js', 'jsx', 'ts', 'tsx'].forEach((ext) => {
it(`we can import all components from .${ext}`, async () => {
const $ = await next.render$(`/${ext}`)
expect($('#js').text()).toBe('CompJs')
})
}
)
})
})

View file

@ -1,23 +1,18 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'interception-dynamic-segment-middleware',
{
describe('interception-dynamic-segment-middleware', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should work when interception route is paired with a dynamic segment & middleware', async () => {
const browser = await next.browser('/')
})
await browser.elementByCss('[href="/foo/p/1"]').click()
await check(() => browser.elementById('modal').text(), /intercepted/)
await browser.refresh()
await check(() => browser.elementById('modal').text(), '')
await check(
() => browser.elementById('children').text(),
/not intercepted/
)
})
}
)
it('should work when interception route is paired with a dynamic segment & middleware', async () => {
const browser = await next.browser('/')
await browser.elementByCss('[href="/foo/p/1"]').click()
await check(() => browser.elementById('modal').text(), /intercepted/)
await browser.refresh()
await check(() => browser.elementById('modal').text(), '')
await check(() => browser.elementById('children').text(), /not intercepted/)
})
})

View file

@ -1,23 +1,18 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'interception-dynamic-segment',
{
describe('interception-dynamic-segment', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should work when interception route is paired with a dynamic segment', async () => {
const browser = await next.browser('/')
})
await browser.elementByCss('[href="/foo/1"]').click()
await check(() => browser.elementById('modal').text(), /intercepted/)
await browser.refresh()
await check(() => browser.elementById('modal').text(), '')
await check(
() => browser.elementById('children').text(),
/not intercepted/
)
})
}
)
it('should work when interception route is paired with a dynamic segment', async () => {
const browser = await next.browser('/')
await browser.elementByCss('[href="/foo/1"]').click()
await check(() => browser.elementById('modal').text(), /intercepted/)
await browser.refresh()
await check(() => browser.elementById('modal').text(), '')
await check(() => browser.elementById('children').text(), /not intercepted/)
})
})

View file

@ -1,90 +1,92 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'interception-middleware-rewrite',
{
describe('interception-middleware-rewrite', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
// TODO: remove after deployment handling is updated
skipDeployment: true,
},
({ next }) => {
it('should support intercepting routes with a middleware rewrite', async () => {
const browser = await next.browser('/')
})
await check(() => browser.waitForElementByCss('#children').text(), 'root')
await check(
() =>
browser
.elementByCss('[href="/feed"]')
.click()
.waitForElementByCss('#modal')
.text(),
'intercepted'
)
await check(
() => browser.refresh().waitForElementByCss('#children').text(),
'not intercepted'
)
await check(() => browser.waitForElementByCss('#modal').text(), '')
})
it('should continue to work after using browser back button and following another intercepting route', async () => {
const browser = await next.browser('/')
await check(() => browser.elementById('children').text(), 'root')
await browser.elementByCss('[href="/photos/1"]').click()
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 1'
)
await browser.back()
await browser.elementByCss('[href="/photos/2"]').click()
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 2'
)
})
it('should continue to show the intercepted page when revisiting it', async () => {
const browser = await next.browser('/')
await check(() => browser.elementById('children').text(), 'root')
await browser.elementByCss('[href="/photos/1"]').click()
// we should be showing the modal and not the page
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 1'
)
await browser.refresh()
// page should show after reloading the browser
await check(
() => browser.elementById('children').text(),
'Page Photo ID: 1'
)
// modal should no longer be showing
await check(() => browser.elementById('modal').text(), '')
await browser.back()
// revisit the same page that was intercepted
await browser.elementByCss('[href="/photos/1"]').click()
// ensure that we're still showing the modal and not the page
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 1'
)
// page content should not have changed
await check(() => browser.elementById('children').text(), 'root')
})
if (skipped) {
return
}
)
it('should support intercepting routes with a middleware rewrite', async () => {
const browser = await next.browser('/')
await check(() => browser.waitForElementByCss('#children').text(), 'root')
await check(
() =>
browser
.elementByCss('[href="/feed"]')
.click()
.waitForElementByCss('#modal')
.text(),
'intercepted'
)
await check(
() => browser.refresh().waitForElementByCss('#children').text(),
'not intercepted'
)
await check(() => browser.waitForElementByCss('#modal').text(), '')
})
it('should continue to work after using browser back button and following another intercepting route', async () => {
const browser = await next.browser('/')
await check(() => browser.elementById('children').text(), 'root')
await browser.elementByCss('[href="/photos/1"]').click()
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 1'
)
await browser.back()
await browser.elementByCss('[href="/photos/2"]').click()
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 2'
)
})
it('should continue to show the intercepted page when revisiting it', async () => {
const browser = await next.browser('/')
await check(() => browser.elementById('children').text(), 'root')
await browser.elementByCss('[href="/photos/1"]').click()
// we should be showing the modal and not the page
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 1'
)
await browser.refresh()
// page should show after reloading the browser
await check(
() => browser.elementById('children').text(),
'Page Photo ID: 1'
)
// modal should no longer be showing
await check(() => browser.elementById('modal').text(), '')
await browser.back()
// revisit the same page that was intercepted
await browser.elementByCss('[href="/photos/1"]').click()
// ensure that we're still showing the modal and not the page
await check(
() => browser.elementById('modal').text(),
'Intercepted Photo ID: 1'
)
// page content should not have changed
await check(() => browser.elementById('children').text(), 'root')
})
})

View file

@ -1,41 +1,39 @@
import { createNextDescribe } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'interception-routes-root-catchall',
{
describe('interception-routes-root-catchall', () => {
const { next } = nextTestSetup({
files: __dirname,
},
({ next }) => {
it('should support having a root catch-all and a catch-all in a parallel route group', async () => {
const browser = await next.browser('/')
await browser.elementByCss('[href="/items/1"]').click()
})
// this triggers the /items route interception handling
await check(
() => browser.elementById('slot').text(),
/Intercepted Modal Page. Id: 1/
)
await browser.refresh()
it('should support having a root catch-all and a catch-all in a parallel route group', async () => {
const browser = await next.browser('/')
await browser.elementByCss('[href="/items/1"]').click()
// no longer intercepted, using the page
await check(() => browser.elementById('slot').text(), /default @modal/)
await check(
() => browser.elementById('children').text(),
/Regular Item Page. Id: 1/
)
})
// this triggers the /items route interception handling
await check(
() => browser.elementById('slot').text(),
/Intercepted Modal Page. Id: 1/
)
await browser.refresh()
it('should handle non-intercepted catch-all pages', async () => {
const browser = await next.browser('/')
// no longer intercepted, using the page
await check(() => browser.elementById('slot').text(), /default @modal/)
await check(
() => browser.elementById('children').text(),
/Regular Item Page. Id: 1/
)
})
// there's no explicit page for /foobar. This will trigger the catchall [...slug] page
await browser.elementByCss('[href="/foobar"]').click()
await check(() => browser.elementById('slot').text(), /default @modal/)
await check(
() => browser.elementById('children').text(),
/Root Catch-All Page/
)
})
}
)
it('should handle non-intercepted catch-all pages', async () => {
const browser = await next.browser('/')
// there's no explicit page for /foobar. This will trigger the catchall [...slug] page
await browser.elementByCss('[href="/foobar"]').click()
await check(() => browser.elementById('slot').text(), /default @modal/)
await check(
() => browser.elementById('children').text(),
/Root Catch-All Page/
)
})
})

Some files were not shown because too many files have changed in this diff Show more