-
Notifications
You must be signed in to change notification settings - Fork 27k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds tests to cover some of the new erroring semantics around dynamic…
…ally tracked data reads
- Loading branch information
Showing
14 changed files
with
725 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,295 @@ | ||
import { createNextDescribe } from 'e2e-utils' | ||
import { getRedboxHeader, hasRedbox } from 'next-test-utils' | ||
|
||
process.env.__TEST_SENTINEL = 'build' | ||
|
||
createNextDescribe( | ||
'dynamic-data', | ||
{ | ||
files: __dirname + '/fixtures/main', | ||
skipStart: true, | ||
}, | ||
({ next, isNextDev, isNextDeploy }) => { | ||
if (isNextDeploy) { | ||
it.skip('should not run in next deploy', () => {}) | ||
return | ||
} | ||
|
||
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 susupense 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 susupense 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 susupense boundary in fallback state | ||
expect($('#boundary').html()).toBeNull() | ||
} else if (process.env.__NEXT_EXPERIMENTAL_PPR) { | ||
// in PPR with force | ||
// @TODO this should actually be build but there is a bug in how we do segment level dynamic in PPR at the moment | ||
// see not 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 susupense 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 susupense 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 susupense 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 () => { | ||
console.log('=========================') | ||
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') | ||
}) | ||
} | ||
) | ||
|
||
createNextDescribe( | ||
'dynamic-data with dynamic = "error"', | ||
{ | ||
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() | ||
}) | ||
|
||
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: Page with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`' | ||
) | ||
} finally { | ||
await browser.close() | ||
} | ||
|
||
browser = await next.browser('/headers?foo=foosearch') | ||
try { | ||
expect(await hasRedbox(browser)).toBe(true) | ||
expect(await getRedboxHeader(browser)).toMatch( | ||
'Error: Page with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`' | ||
) | ||
} finally { | ||
await browser.close() | ||
} | ||
|
||
browser = await next.browser('/search?foo=foosearch') | ||
try { | ||
expect(await hasRedbox(browser)).toBe(true) | ||
expect(await getRedboxHeader(browser)).toMatch( | ||
'Error: Page 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: Page with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`' | ||
) | ||
expect(next.cliOutput).toMatch( | ||
'Error: Page with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`' | ||
) | ||
expect(next.cliOutput).toMatch( | ||
'Error: Page with `dynamic = "error"` couldn\'t be rendered statically because it used `searchParams`.' | ||
) | ||
}) | ||
} | ||
} | ||
) | ||
|
||
createNextDescribe( | ||
'dynamic-data inside cache scope', | ||
{ | ||
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() | ||
}) | ||
|
||
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: used "cookies" inside a function cached with "unstable_cache(...)".' | ||
) | ||
} finally { | ||
await browser.close() | ||
} | ||
|
||
browser = await next.browser('/headers') | ||
try { | ||
expect(await hasRedbox(browser)).toBe(true) | ||
expect(await getRedboxHeader(browser)).toMatch( | ||
'Error: 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: used "cookies" inside a function cached with "unstable_cache(...)".' | ||
) | ||
expect(next.cliOutput).toMatch( | ||
'Error: used "headers" inside a function cached with "unstable_cache(...)".' | ||
) | ||
}) | ||
} | ||
} | ||
) |
35 changes: 35 additions & 0 deletions
35
test/e2e/app-dir/dynamic-data/fixtures/cache-scoped/app/cookies/page.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { cookies as nextCookies } from 'next/headers' | ||
import { unstable_cache as cache } from 'next/cache' | ||
|
||
const cookies = cache(() => nextCookies()) | ||
|
||
export default async function Page({ searchParams }) { | ||
console.log('cookies()', await cookies()) | ||
return ( | ||
<div> | ||
<section> | ||
This example uses `cookies()` but is configured with `dynamic = 'error'` | ||
which should cause the page to fail to build | ||
</section> | ||
<section id="cookies"> | ||
<h3>cookies</h3> | ||
{cookies() | ||
.getAll() | ||
.map((cookie) => { | ||
const key = cookie.name | ||
let value = cookie.value | ||
|
||
if (key === 'userCache') { | ||
value = value.slice(0, 10) + '...' | ||
} | ||
return ( | ||
<div key={key}> | ||
<h4>{key}</h4> | ||
<pre className={key}>{value}</pre> | ||
</div> | ||
) | ||
})} | ||
</section> | ||
</div> | ||
) | ||
} |
29 changes: 29 additions & 0 deletions
29
test/e2e/app-dir/dynamic-data/fixtures/cache-scoped/app/headers/page.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { headers as nextHeaders } from 'next/headers' | ||
import { unstable_cache as cache } from 'next/cache' | ||
|
||
const headers = cache(() => nextHeaders()) | ||
|
||
export default async function Page() { | ||
return ( | ||
<div> | ||
<section> | ||
This example uses `headers()` but is configured with `dynamic = 'error'` | ||
which should cause the page to fail to build | ||
</section> | ||
<section id="headers"> | ||
<h3>headers</h3> | ||
{Array.from(await headers()) | ||
.entries() | ||
.map(([key, value]) => { | ||
if (key === 'cookie') return null | ||
return ( | ||
<div key={key}> | ||
<h4>{key}</h4> | ||
<pre className={key}>{value}</pre> | ||
</div> | ||
) | ||
})} | ||
</section> | ||
</div> | ||
) | ||
} |
23 changes: 23 additions & 0 deletions
23
test/e2e/app-dir/dynamic-data/fixtures/cache-scoped/app/layout.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Suspense } from 'react' | ||
|
||
export default async function Layout({ children }) { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<title>app-dynamic-data</title> | ||
</head> | ||
<body> | ||
<p> | ||
This test fixture helps us assert that accessing dynamic data in | ||
various scopes and with various `dynamic` configurations works as | ||
intended | ||
</p> | ||
<main> | ||
<Suspense fallback={<div id="boundary">loading...</div>}> | ||
{children} | ||
</Suspense> | ||
</main> | ||
</body> | ||
</html> | ||
) | ||
} |
Oops, something went wrong.