rsnext/test/e2e/prerender.test.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2488 lines
86 KiB
TypeScript
Raw Normal View History

import fs from 'fs-extra'
import cookie from 'cookie'
import cheerio from 'cheerio'
import { join, sep } from 'path'
import escapeRegex from 'escape-string-regexp'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import {
check,
fetchViaHTTP,
getBrowserBodyText,
getRedboxHeader,
hasRedbox,
normalizeRegEx,
renderViaHTTP,
retry,
waitFor,
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import stripAnsi from 'strip-ansi'
describe('Prerender', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'prerender/pages')),
'world.txt': new FileRef(join(__dirname, 'prerender/world.txt')),
},
dependencies: {
firebase: '7.14.5',
},
nextConfig: {
async rewrites() {
return [
{
source: '/some-rewrite/:item',
destination: '/blog/post-:item',
},
{
source: '/about',
destination: '/lang/en/about',
},
{
source: '/blocked-create',
destination: '/blocking-fallback/blocked-create',
},
]
},
},
})
})
afterAll(() => next.destroy())
async function waitForCacheWrite(
prerenderPath = '',
timeBeforeRevalidateMilliseconds,
retries = 30
) {
for (let i = 0; i < retries; i++) {
const lastRetry = i === retries - 1
const jsonPath = join(
next.testDir,
'.next',
'server',
'pages',
`${prerenderPath}.html`
)
try {
const jsonStats = await fs.stat(jsonPath)
const jsonLastModified = jsonStats.mtime.getTime()
if (timeBeforeRevalidateMilliseconds <= jsonLastModified) {
break
}
throw new Error(
`revalidate cache not past ${timeBeforeRevalidateMilliseconds} received time ${jsonLastModified}`
)
} catch (err) {
if (lastRetry) {
throw err
}
}
await waitFor(500)
}
}
function isCachingHeader(cacheControl) {
return !cacheControl || !/no-store/.test(cacheControl)
}
const expectedManifestRoutes = () => ({
'/': {
dataRoute: `/_next/data/${next.buildId}/index.json`,
initialRevalidateSeconds: 2,
srcRoute: null,
},
'/blog/[post3]': {
dataRoute: `/_next/data/${next.buildId}/blog/[post3].json`,
initialRevalidateSeconds: 10,
srcRoute: '/blog/[post]',
},
'/blog/post-1': {
dataRoute: `/_next/data/${next.buildId}/blog/post-1.json`,
initialRevalidateSeconds: 10,
srcRoute: '/blog/[post]',
},
'/blog/post-2': {
dataRoute: `/_next/data/${next.buildId}/blog/post-2.json`,
initialRevalidateSeconds: 10,
srcRoute: '/blog/[post]',
},
'/blog/post-4': {
dataRoute: `/_next/data/${next.buildId}/blog/post-4.json`,
initialRevalidateSeconds: 10,
srcRoute: '/blog/[post]',
},
'/blog/post-1/comment-1': {
dataRoute: `/_next/data/${next.buildId}/blog/post-1/comment-1.json`,
initialRevalidateSeconds: 2,
srcRoute: '/blog/[post]/[comment]',
},
'/blog/post-2/comment-2': {
dataRoute: `/_next/data/${next.buildId}/blog/post-2/comment-2.json`,
initialRevalidateSeconds: 2,
srcRoute: '/blog/[post]/[comment]',
},
'/blog/post.1': {
dataRoute: `/_next/data/${next.buildId}/blog/post.1.json`,
initialRevalidateSeconds: 10,
srcRoute: '/blog/[post]',
},
'/catchall-explicit/another/value': {
dataRoute: `/_next/data/${next.buildId}/catchall-explicit/another/value.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-explicit/first': {
dataRoute: `/_next/data/${next.buildId}/catchall-explicit/first.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-explicit/hello/another': {
dataRoute: `/_next/data/${next.buildId}/catchall-explicit/hello/another.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-explicit/second': {
dataRoute: `/_next/data/${next.buildId}/catchall-explicit/second.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-explicit/[first]/[second]': {
dataRoute: `/_next/data/${next.buildId}/catchall-explicit/[first]/[second].json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-explicit/[third]/[fourth]': {
dataRoute: `/_next/data/${next.buildId}/catchall-explicit/[third]/[fourth].json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-optional': {
dataRoute: `/_next/data/${next.buildId}/catchall-optional.json`,
initialRevalidateSeconds: false,
srcRoute: '/catchall-optional/[[...slug]]',
},
'/catchall-optional/value': {
dataRoute: `/_next/data/${next.buildId}/catchall-optional/value.json`,
initialRevalidateSeconds: false,
srcRoute: '/catchall-optional/[[...slug]]',
},
'/large-page-data': {
dataRoute: `/_next/data/${next.buildId}/large-page-data.json`,
initialRevalidateSeconds: false,
srcRoute: null,
},
'/another': {
dataRoute: `/_next/data/${next.buildId}/another.json`,
initialRevalidateSeconds: 1,
srcRoute: null,
},
'/preview': {
dataRoute: `/_next/data/${next.buildId}/preview.json`,
initialRevalidateSeconds: false,
srcRoute: null,
},
'/api-docs/first': {
dataRoute: `/_next/data/${next.buildId}/api-docs/first.json`,
initialRevalidateSeconds: false,
srcRoute: '/api-docs/[...slug]',
},
'/blocking-fallback-once/404-on-manual-revalidate': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback-once/404-on-manual-revalidate.json`,
initialRevalidateSeconds: false,
srcRoute: '/blocking-fallback-once/[slug]',
},
'/blocking-fallback-some/a': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback-some/a.json`,
initialRevalidateSeconds: 1,
srcRoute: '/blocking-fallback-some/[slug]',
},
'/blocking-fallback-some/b': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback-some/b.json`,
initialRevalidateSeconds: 1,
srcRoute: '/blocking-fallback-some/[slug]',
},
'/blocking-fallback/lots-of-data': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback/lots-of-data.json`,
initialRevalidateSeconds: false,
srcRoute: '/blocking-fallback/[slug]',
},
'/blocking-fallback/test-errors-1': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback/test-errors-1.json`,
initialRevalidateSeconds: 1,
srcRoute: '/blocking-fallback/[slug]',
},
'/blog': {
dataRoute: `/_next/data/${next.buildId}/blog.json`,
initialRevalidateSeconds: 10,
srcRoute: null,
},
'/default-revalidate': {
dataRoute: `/_next/data/${next.buildId}/default-revalidate.json`,
initialRevalidateSeconds: false,
srcRoute: null,
},
'/dynamic/[first]': {
dataRoute: `/_next/data/${next.buildId}/dynamic/[first].json`,
initialRevalidateSeconds: false,
srcRoute: '/dynamic/[slug]',
},
'/dynamic/[second]': {
dataRoute: `/_next/data/${next.buildId}/dynamic/[second].json`,
initialRevalidateSeconds: false,
srcRoute: '/dynamic/[slug]',
},
// TODO: investigate index/index
// '/index': {
// dataRoute: `/_next/data/${next.buildId}/index/index.json`,
// initialRevalidateSeconds: false,
// srcRoute: null,
// },
'/lang/de/about': {
dataRoute: `/_next/data/${next.buildId}/lang/de/about.json`,
initialRevalidateSeconds: false,
srcRoute: '/lang/[lang]/about',
},
'/lang/en/about': {
dataRoute: `/_next/data/${next.buildId}/lang/en/about.json`,
initialRevalidateSeconds: false,
srcRoute: '/lang/[lang]/about',
},
'/lang/es/about': {
dataRoute: `/_next/data/${next.buildId}/lang/es/about.json`,
initialRevalidateSeconds: false,
srcRoute: '/lang/[lang]/about',
},
'/lang/fr/about': {
dataRoute: `/_next/data/${next.buildId}/lang/fr/about.json`,
initialRevalidateSeconds: false,
srcRoute: '/lang/[lang]/about',
},
'/something': {
dataRoute: `/_next/data/${next.buildId}/something.json`,
initialRevalidateSeconds: false,
srcRoute: null,
},
'/catchall/another/value': {
dataRoute: `/_next/data/${next.buildId}/catchall/another/value.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
'/catchall/first': {
dataRoute: `/_next/data/${next.buildId}/catchall/first.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
'/catchall/second': {
dataRoute: `/_next/data/${next.buildId}/catchall/second.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
'/catchall/hello/another': {
dataRoute: `/_next/data/${next.buildId}/catchall/hello/another.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
})
const navigateTest = (isDev = false) => {
it('should navigate between pages successfully', async () => {
const toBuild = [
'/',
'/another',
'/something',
'/normal',
'/blog/post-1',
'/blog/post-1/comment-1',
'/catchall/first',
]
await waitFor(2500)
await Promise.all(toBuild.map((pg) => renderViaHTTP(next.url, pg)))
const browser = await webdriver(next.url, '/')
let text = await browser.elementByCss('p').text()
expect(text).toMatch(/hello.*?world/)
// go to /another
async function goFromHomeToAnother() {
await browser.eval('window.beforeAnother = true')
await browser.elementByCss('#another').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('p').text()
expect(await browser.eval('window.beforeAnother')).toBe(true)
expect(text).toMatch(/hello.*?world/)
}
await goFromHomeToAnother()
// go to /
async function goFromAnotherToHome() {
await browser.eval('window.didTransition = 1')
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#another')
text = await browser.elementByCss('p').text()
expect(text).toMatch(/hello.*?world/)
expect(await browser.eval('window.didTransition')).toBe(1)
}
await goFromAnotherToHome()
// Client-side SSG data caching test
// eslint-disable-next-line no-lone-blocks
{
// Let revalidation period lapse
await waitFor(2000)
// Trigger revalidation (visit page)
await goFromHomeToAnother()
const snapTime = await browser.elementByCss('#anotherTime').text()
// Wait for revalidation to finish
await waitFor(2000)
// Re-visit page
await goFromAnotherToHome()
await goFromHomeToAnother()
const nextTime = await browser.elementByCss('#anotherTime').text()
// in dev the time should always differ as we don't cache
// in production the time may differ or may not depending
// on if fresh content beat the stale content
if (isDev) {
expect(snapTime).not.toMatch(nextTime)
}
// Reset to Home for next test
await goFromAnotherToHome()
}
// go to /something
await browser.elementByCss('#something').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('p').text()
expect(text).toMatch(/hello.*?world/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#post-1')
// go to /blog/post-1
await browser.elementByCss('#post-1').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('p').text()
expect(text).toMatch(/Post:.*?post-1/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')
// TODO: investigate index/index
// go to /index
// await browser.elementByCss('#to-nested-index').click()
// await browser.waitForElementByCss('#home')
// text = await browser.elementByCss('p').text()
// expect(text).toMatch(/hello nested index/)
// go to /
// await browser.elementByCss('#home').click()
// await browser.waitForElementByCss('#comment-1')
// go to /catchall-optional
await browser.elementByCss('#catchall-optional-root').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('p').text()
expect(text).toMatch(/Catch all: \[\]/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')
// go to /dynamic/[first]
await browser.elementByCss('#dynamic-first').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#param').text()
expect(text).toMatch(/Hi \[first\]!/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')
// go to /dynamic/[second]
await browser.elementByCss('#dynamic-second').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#param').text()
expect(text).toMatch(/Hi \[second\]!/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')
// go to /catchall-explicit/[first]/[second]
await browser.elementByCss('#catchall-explicit-string').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#catchall').text()
expect(text).toMatch(/Hi \[first\] \[second\]/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')
// go to /catchall-explicit/[first]/[second]
await browser.elementByCss('#catchall-explicit-object').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#catchall').text()
expect(text).toMatch(/Hi \[third\] \[fourth\]/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')
// go to /catchall-optional/value
await browser.elementByCss('#catchall-optional-value').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('p').text()
expect(text).toMatch(/Catch all: \[value\]/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')
// go to /blog/post-1/comment-1
await browser.elementByCss('#comment-1').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('p:nth-child(2)').text()
expect(text).toMatch(/Comment:.*?comment-1/)
expect(await browser.eval('window.didTransition')).toBe(1)
// go to /catchall/first
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#to-catchall')
await browser.elementByCss('#to-catchall').click()
await browser.waitForElementByCss('#catchall')
text = await browser.elementByCss('#catchall').text()
expect(text).toMatch(/Hi.*?first/)
expect(await browser.eval('window.didTransition')).toBe(1)
await browser.close()
})
}
const runTests = (isDev = false, isDeploy) => {
navigateTest(isDev)
it('should respond with 405 for POST to static page', async () => {
const res = await fetchViaHTTP(next.url, '/', undefined, {
method: 'POST',
})
expect(res.status).toBe(405)
if (!isDeploy) {
expect(await res.text()).toContain('Method Not Allowed')
}
})
it('should SSR normal page correctly', async () => {
const html = await renderViaHTTP(next.url, '/')
expect(html).toMatch(/hello.*?world/)
})
it('should SSR incremental page correctly', async () => {
const html = await renderViaHTTP(next.url, '/blog/post-1')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(false)
expect(html).toMatch(/Post:.*?post-1/)
})
it('should SSR blocking path correctly (blocking)', async () => {
const html = await renderViaHTTP(
next.url,
'/blocking-fallback/random-path'
)
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(false)
expect($('p').text()).toBe('Post: random-path')
})
it('should SSR blocking path correctly (pre-rendered)', async () => {
const html = await renderViaHTTP(next.url, '/blocking-fallback-some/a')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(false)
expect($('p').text()).toBe('Post: a')
})
it('should have gsp in __NEXT_DATA__', async () => {
const html = await renderViaHTTP(next.url, '/')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).gsp).toBe(true)
})
it('should not have gsp in __NEXT_DATA__ for non-GSP page', async () => {
const html = await renderViaHTTP(next.url, '/normal')
const $ = cheerio.load(html)
expect('gsp' in JSON.parse($('#__NEXT_DATA__').text())).toBe(false)
})
it('should not supply query values to params or useRouter non-dynamic page SSR', async () => {
const html = await renderViaHTTP(next.url, '/something?hello=world')
const $ = cheerio.load(html)
const query = $('#query').text()
expect(JSON.parse(query)).toEqual({})
const params = $('#params').text()
expect(JSON.parse(params)).toEqual({})
})
it('should not supply query values to params in /_next/data request', async () => {
const data = JSON.parse(
await renderViaHTTP(
next.url,
`/_next/data/${next.buildId}/something.json?hello=world`
)
)
expect(data.pageProps.params).toEqual({})
})
it('should not supply query values to params or useRouter dynamic page SSR', async () => {
const html = await renderViaHTTP(next.url, '/blog/post-1?hello=world')
const $ = cheerio.load(html)
const params = $('#params').text()
expect(JSON.parse(params)).toEqual({ post: 'post-1' })
const query = $('#query').text()
expect(JSON.parse(query)).toEqual({ post: 'post-1' })
})
it('should return data correctly', async () => {
const data = JSON.parse(
await renderViaHTTP(
next.url,
`/_next/data/${next.buildId}/something.json`
)
)
expect(data.pageProps.world).toBe('world')
})
it('should return data correctly for dynamic page', async () => {
const data = JSON.parse(
await renderViaHTTP(
next.url,
`/_next/data/${next.buildId}/blog/post-1.json`
)
)
expect(data.pageProps.post).toBe('post-1')
})
it('should return data correctly for dynamic page (non-seeded)', async () => {
const data = JSON.parse(
await renderViaHTTP(
next.url,
`/_next/data/${next.buildId}/blog/post-3.json`
)
)
expect(data.pageProps.post).toBe('post-3')
})
if (!isDev) {
it('should use correct caching headers for a revalidate page', async () => {
const initialRes = await fetchViaHTTP(next.url, '/')
expect(initialRes.headers.get('cache-control')).toBe(
isDeploy
? 'public, max-age=0, must-revalidate'
: 's-maxage=2, stale-while-revalidate'
)
})
}
it('should navigate to a normal page and back', async () => {
const browser = await webdriver(next.url, '/')
let text = await browser.elementByCss('p').text()
expect(text).toMatch(/hello.*?world/)
await browser.elementByCss('#normal').click()
await browser.waitForElementByCss('#normal-text')
text = await browser.elementByCss('#normal-text').text()
expect(text).toMatch(/a normal page/)
})
it('should parse query values on mount correctly', async () => {
const browser = await webdriver(next.url, '/blog/post-1?another=value')
const text = await browser.elementByCss('#query').text()
expect(text).toMatch(/another.*?value/)
expect(text).toMatch(/post.*?post-1/)
})
it('should reload page on failed data request', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.beforeClick = "abc"')
await browser.elementByCss('#broken-post').click()
expect(
await check(() => browser.eval('window.beforeClick'), {
test(v) {
return v !== 'abc'
},
})
).toBe(true)
})
it('should SSR dynamic page with brackets in param as object', async () => {
const html = await renderViaHTTP(next.url, '/dynamic/[first]')
const $ = cheerio.load(html)
expect($('#param').text()).toMatch(/Hi \[first\]!/)
})
it('should navigate to dynamic page with brackets in param as object', async () => {
const browser = await webdriver(next.url, '/')
await browser.elementByCss('#dynamic-first').click()
await browser.waitForElementByCss('#param')
const value = await browser.elementByCss('#param').text()
expect(value).toMatch(/Hi \[first\]!/)
})
it('should SSR dynamic page with brackets in param as string', async () => {
const html = await renderViaHTTP(next.url, '/dynamic/[second]')
const $ = cheerio.load(html)
expect($('#param').text()).toMatch(/Hi \[second\]!/)
})
it('should navigate to dynamic page with brackets in param as string', async () => {
const browser = await webdriver(next.url, '/')
await browser.elementByCss('#dynamic-second').click()
await browser.waitForElementByCss('#param')
const value = await browser.elementByCss('#param').text()
expect(value).toMatch(/Hi \[second\]!/)
})
it('should not return data for fallback: false and missing dynamic page', async () => {
const res1 = await fetchViaHTTP(
next.url,
`/_next/data/${next.buildId}/dynamic/oopsie.json`
)
expect(res1.status).toBe(404)
await waitFor(500)
const res2 = await fetchViaHTTP(
next.url,
`/_next/data/${next.buildId}/dynamic/oopsie.json`
)
expect(res2.status).toBe(404)
await waitFor(500)
const res3 = await fetchViaHTTP(
next.url,
`/_next/data/${next.buildId}/dynamic/oopsie.json`
)
expect(res3.status).toBe(404)
})
it('should server prerendered path correctly for SSG pages that starts with api-docs', async () => {
const html = await renderViaHTTP(next.url, '/api-docs/first')
const $ = cheerio.load(html)
expect($('#api-docs').text()).toBe('API Docs')
expect(JSON.parse($('#props').text())).toEqual({
hello: 'world',
})
})
it('should render correctly for SSG pages that starts with api-docs', async () => {
const browser = await webdriver(next.url, '/api-docs/second')
await browser.waitForElementByCss('#api-docs')
expect(await browser.elementByCss('#api-docs').text()).toBe('API Docs')
expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({
hello: 'world',
})
})
it('should return data correctly for SSG pages that starts with api-docs', async () => {
const data = await renderViaHTTP(
next.url,
`/_next/data/${next.buildId}/api-docs/first.json`
)
const { pageProps } = JSON.parse(data)
expect(pageProps).toEqual({
hello: 'world',
})
})
it('should SSR catch-all page with brackets in param as string', async () => {
const html = await renderViaHTTP(
next.url,
'/catchall-explicit/[first]/[second]'
)
const $ = cheerio.load(html)
expect($('#catchall').text()).toMatch(/Hi \[first\] \[second\]/)
})
it('should navigate to catch-all page with brackets in param as string', async () => {
const browser = await webdriver(next.url, '/')
await browser.elementByCss('#catchall-explicit-string').click()
await browser.waitForElementByCss('#catchall')
const value = await browser.elementByCss('#catchall').text()
expect(value).toMatch(/Hi \[first\] \[second\]/)
})
it('should SSR catch-all page with brackets in param as object', async () => {
const html = await renderViaHTTP(
next.url,
'/catchall-explicit/[third]/[fourth]'
)
const $ = cheerio.load(html)
expect($('#catchall').text()).toMatch(/Hi \[third\] \[fourth\]/)
})
it('should navigate to catch-all page with brackets in param as object', async () => {
const browser = await webdriver(next.url, '/')
await browser.elementByCss('#catchall-explicit-object').click()
await browser.waitForElementByCss('#catchall')
const value = await browser.elementByCss('#catchall').text()
expect(value).toMatch(/Hi \[third\] \[fourth\]/)
})
if ((global as any).isNextStart) {
// TODO: dev currently renders this page as blocking, meaning it shows the
// server error instead of continuously retrying. Do we want to change this?
it.skip('should reload page on failed data request, and retry', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.beforeClick = "abc"')
await browser.elementByCss('#broken-at-first-post').click()
expect(
await check(() => browser.eval('window.beforeClick'), {
test(v) {
return v !== 'abc'
},
})
).toBe(true)
const text = await browser.elementByCss('#params').text()
expect(text).toMatch(/post.*?post-999/)
})
}
it('should support prerendered catchall route', async () => {
const html = await renderViaHTTP(next.url, '/catchall/another/value')
const $ = cheerio.load(html)
expect(
JSON.parse(cheerio.load(html)('#__NEXT_DATA__').text()).isFallback
).toBe(false)
expect($('#catchall').text()).toMatch(/Hi.*?another value/)
})
it('should support lazy catchall route', async () => {
const html = await renderViaHTTP(next.url, '/catchall/notreturnedinpaths')
const $ = cheerio.load(html)
expect($('#catchall').text()).toBe('fallback')
// hydration
const browser = await webdriver(next.url, '/catchall/delayby3s')
const text1 = await browser.elementByCss('#catchall').text()
expect(text1).toBe('fallback')
await check(
() => browser.elementByCss('#catchall').text(),
/Hi.*?delayby3s/
)
})
it('should support nested lazy catchall route', async () => {
// We will render fallback for a "lazy" route
const html = await renderViaHTTP(
next.url,
'/catchall/notreturnedinpaths/nested'
)
const $ = cheerio.load(html)
expect($('#catchall').text()).toBe('fallback')
// hydration
const browser = await webdriver(next.url, '/catchall/delayby3s/nested')
const text1 = await browser.elementByCss('#catchall').text()
expect(text1).toBe('fallback')
await check(
() => browser.elementByCss('#catchall').text(),
/Hi.*?delayby3s nested/
)
})
it('should support prerendered catchall-explicit route (nested)', async () => {
const html = await renderViaHTTP(
next.url,
'/catchall-explicit/another/value'
)
const $ = cheerio.load(html)
expect(
JSON.parse(cheerio.load(html)('#__NEXT_DATA__').text()).isFallback
).toBe(false)
expect($('#catchall').text()).toMatch(/Hi.*?another value/)
})
it('should support prerendered catchall-explicit route (single)', async () => {
const html = await renderViaHTTP(next.url, '/catchall-explicit/second')
const $ = cheerio.load(html)
expect(
JSON.parse(cheerio.load(html)('#__NEXT_DATA__').text()).isFallback
).toBe(false)
expect($('#catchall').text()).toMatch(/Hi.*?second/)
})
it('should handle fallback only page correctly HTML', async () => {
const browser = await webdriver(next.url, '/fallback-only/first%2Fpost', {
waitHydration: false,
})
const text = await browser.elementByCss('p').text()
expect(text).toContain('hi fallback')
// wait for fallback data to load
await check(() => browser.elementByCss('p').text(), /Post/)
// check fallback data
const post = await browser.elementByCss('p').text()
const query = JSON.parse(await browser.elementByCss('#query').text())
const params = JSON.parse(await browser.elementByCss('#params').text())
expect(post).toContain('first/post')
expect(params).toEqual({
slug: 'first/post',
})
expect(query).toEqual(params)
})
it('should handle fallback only page correctly data', async () => {
const data = JSON.parse(
await renderViaHTTP(
next.url,
`/_next/data/${next.buildId}/fallback-only/second%2Fpost.json`
)
)
expect(data.pageProps.params).toEqual({
slug: 'second/post',
})
})
it('should 404 for a missing catchall explicit route', async () => {
const res = await fetchViaHTTP(
next.url,
'/catchall-explicit/notreturnedinpaths'
)
expect(res.status).toBe(404)
const html = await res.text()
expect(html).toMatch(/This page could not be found/)
})
it('should 404 for an invalid data url', async () => {
const res = await fetchViaHTTP(next.url, `/_next/data/${next.buildId}`)
// when deployed this will match due to `index.json` matching the
// directory itself
if (!isDeploy) {
expect(res.status).toBe(404)
}
})
it('should allow rewriting to SSG page with fallback: false', async () => {
const html = await renderViaHTTP(next.url, '/about')
expect(html).toMatch(/About:.*?en/)
})
it("should allow rewriting to SSG page with fallback: 'blocking'", async () => {
const html = await renderViaHTTP(next.url, '/blocked-create')
expect(html).toMatch(/Post:.*?blocked-create/)
})
it('should fetch /_next/data correctly with mismatched href and as', async () => {
const browser = await webdriver(next.url, '/')
if (!isDev) {
await browser.eval(() =>
document.querySelector('#to-rewritten-ssg').scrollIntoView()
)
await check(async () => {
const hrefs = await browser.eval(
`Object.keys(window.next.router.sdc)`
)
hrefs.sort()
expect(
hrefs.map((href) =>
new URL(href).pathname.replace(/^\/_next\/data\/[^/]+/, '')
)
).toContainEqual('/lang/en/about.json')
return 'yes'
}, 'yes')
}
await browser.eval('window.beforeNav = "hi"')
await browser.elementByCss('#to-rewritten-ssg').click()
await browser.waitForElementByCss('#about')
expect(await browser.eval('window.beforeNav')).toBe('hi')
expect(await browser.elementByCss('#about').text()).toBe('About: en')
})
it('should not error when rewriting to fallback dynamic SSG page', async () => {
const item = Math.round(Math.random() * 100)
const browser = await webdriver(next.url, `/some-rewrite/${item}`)
await check(
() => browser.elementByCss('p').text(),
new RegExp(`Post: post-${item}`)
)
expect(JSON.parse(await browser.elementByCss('#params').text())).toEqual({
post: `post-${item}`,
})
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
post: `post-${item}`,
})
})
it('should show warning when large amount of page data is returned', async () => {
await renderViaHTTP(next.url, '/large-page-data')
await check(
() => next.cliOutput,
/Warning: data for page "\/large-page-data" is 256 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance/
)
await renderViaHTTP(next.url, '/blocking-fallback/lots-of-data')
await check(
() => next.cliOutput,
/Warning: data for page "\/blocking-fallback\/\[slug\]" \(path "\/blocking-fallback\/lots-of-data"\) is 256 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance/
)
})
if ((global as any).isNextDev) {
it('should show warning every time page with large amount of page data is returned', async () => {
await renderViaHTTP(next.url, '/large-page-data-ssr')
await check(
() => next.cliOutput,
/Warning: data for page "\/large-page-data-ssr" is 256 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance/
)
const outputIndex = next.cliOutput.length
await renderViaHTTP(next.url, '/large-page-data-ssr')
await check(
() => next.cliOutput.slice(outputIndex),
/Warning: data for page "\/large-page-data-ssr" is 256 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance/
)
})
}
if ((global as any).isNextStart) {
it('should only show warning once per page when large amount of page data is returned', async () => {
await renderViaHTTP(next.url, '/large-page-data-ssr')
await check(
() => next.cliOutput,
/Warning: data for page "\/large-page-data-ssr" is 256 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance/
)
const outputIndex = next.cliOutput.length
await renderViaHTTP(next.url, '/large-page-data-ssr')
expect(next.cliOutput.slice(outputIndex)).not.toInclude(
'Warning: data for page'
)
})
}
if ((global as any).isNextDev) {
it('should not show warning from url prop being returned', async () => {
const urlPropPage = 'pages/url-prop.js'
await next.patchFile(
urlPropPage,
`
export async function getStaticProps() {
return {
props: {
url: 'something'
}
}
}
export default ({ url }) => <p>url: {url}</p>
`
)
const html = await renderViaHTTP(next.url, '/url-prop')
await next.deleteFile(urlPropPage)
expect(next.cliOutput).not.toMatch(
/The prop `url` is a reserved prop in Next.js for legacy reasons and will be overridden on page \/url-prop/
)
expect(html).toMatch(/url:.*?something/)
})
it('should always show fallback for page not in getStaticPaths', async () => {
const html = await renderViaHTTP(next.url, '/blog/post-321')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(true)
// make another request to ensure it still is
const html2 = await renderViaHTTP(next.url, '/blog/post-321')
const $2 = cheerio.load(html2)
expect(JSON.parse($2('#__NEXT_DATA__').text()).isFallback).toBe(true)
})
it('should not show fallback for page in getStaticPaths', async () => {
const html = await renderViaHTTP(next.url, '/blog/post-1')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(false)
// make another request to ensure it's still not
const html2 = await renderViaHTTP(next.url, '/blog/post-1')
const $2 = cheerio.load(html2)
expect(JSON.parse($2('#__NEXT_DATA__').text()).isFallback).toBe(false)
})
it('should never show fallback for page not in getStaticPaths when blocking', async () => {
const html = await renderViaHTTP(
next.url,
'/blocking-fallback-some/asf'
)
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(false)
// make another request to ensure it still is
const html2 = await renderViaHTTP(
next.url,
'/blocking-fallback-some/asf'
)
const $2 = cheerio.load(html2)
expect(JSON.parse($2('#__NEXT_DATA__').text()).isFallback).toBe(false)
})
it('should not show fallback for page in getStaticPaths when blocking', async () => {
const html = await renderViaHTTP(next.url, '/blocking-fallback-some/b')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(false)
// make another request to ensure it's still not
const html2 = await renderViaHTTP(next.url, '/blocking-fallback-some/b')
const $2 = cheerio.load(html2)
expect(JSON.parse($2('#__NEXT_DATA__').text()).isFallback).toBe(false)
})
Rename process.env.TURBOPACK -> process.env.TURBOPACK_DEV in test skips (#63665) ## What? Follow-up to #63653. <!-- 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 # --> Closes NEXT-2911
2024-03-25 14:17:56 +01:00
it('should log error in console and browser in development mode', async () => {
const indexPage = 'pages/index.js'
const origContent = await next.readFile(indexPage)
const browser = await webdriver(next.url, '/')
expect(await browser.elementByCss('p').text()).toMatch(/hello.*?world/)
await next.patchFile(
indexPage,
origContent
.replace('// throw new', 'throw new')
.replace('{/* <div', '<div')
.replace('</div> */}', '</div>')
)
fix flaky prerender test (#63826) This test doesn't clean up the file that it patches in the event any of the assertions fail. [x-ref](https://github.com/vercel/next.js/actions/runs/8468863964/job/23204091268?pr=63819#step:27:1227) <!-- 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 # -->
2024-03-28 16:57:12 +01:00
try {
await browser.waitForElementByCss('#after-change')
// we need to reload the page to trigger getStaticProps
await browser.refresh()
expect(await hasRedbox(browser)).toBe(true)
const errOverlayContent = await getRedboxHeader(browser)
const errorMsg = /oops from getStaticProps/
expect(next.cliOutput).toMatch(errorMsg)
expect(errOverlayContent).toMatch(errorMsg)
} finally {
await next.patchFile(indexPage, origContent)
}
})
it('should always call getStaticProps without caching in dev', async () => {
const initialRes = await fetchViaHTTP(next.url, '/something')
expect(isCachingHeader(initialRes.headers.get('cache-control'))).toBe(
false
)
const initialHtml = await initialRes.text()
expect(initialHtml).toMatch(/hello.*?world/)
const newRes = await fetchViaHTTP(next.url, '/something')
expect(isCachingHeader(newRes.headers.get('cache-control'))).toBe(false)
const newHtml = await newRes.text()
expect(newHtml).toMatch(/hello.*?world/)
expect(initialHtml !== newHtml).toBe(true)
const newerRes = await fetchViaHTTP(next.url, '/something')
expect(isCachingHeader(newerRes.headers.get('cache-control'))).toBe(
false
)
const newerHtml = await newerRes.text()
expect(newerHtml).toMatch(/hello.*?world/)
expect(newHtml !== newerHtml).toBe(true)
})
it('should error on bad object from getStaticProps', async () => {
const indexPage = 'pages/index.js'
const origContent = await next.readFile(indexPage)
await next.patchFile(
indexPage,
origContent.replace(/\/\/ bad-prop/, 'another: true,')
)
await waitFor(1000)
try {
const html = await renderViaHTTP(next.url, '/')
expect(html).toMatch(/Additional keys were returned/)
} finally {
await next.patchFile(indexPage, origContent)
}
})
it('should error on dynamic page without getStaticPaths', async () => {
const curPage = 'pages/temp/[slug].js'
await next.patchFile(
curPage,
`
export async function getStaticProps() {
return {
props: {
hello: 'world'
}
}
}
export default () => 'oops'
`
)
await waitFor(1000)
try {
const html = await renderViaHTTP(next.url, '/temp/hello')
expect(html).toMatch(
/getStaticPaths is required for dynamic SSG pages and is missing for/
)
} finally {
await next.deleteFile(curPage)
}
})
it('should error on dynamic page without getStaticPaths returning fallback property', async () => {
const curPage = 'pages/temp2/[slug].js'
await next.patchFile(
curPage,
`
export async function getStaticPaths() {
return {
paths: []
}
}
export async function getStaticProps() {
return {
props: {
hello: 'world'
}
}
}
export default () => 'oops'
`
)
await waitFor(1000)
try {
const html = await renderViaHTTP(next.url, '/temp2/hello')
expect(html).toMatch(/`fallback` key must be returned from/)
} finally {
await next.deleteFile(curPage)
}
})
it('should not re-call getStaticProps when updating query', async () => {
const browser = await webdriver(next.url, '/something?hello=world')
await waitFor(2000)
const query = await browser.elementByCss('#query').text()
expect(JSON.parse(query)).toEqual({ hello: 'world' })
const {
props: {
pageProps: { random: initialRandom },
},
} = await browser.eval('window.__NEXT_DATA__')
const curRandom = await browser.elementByCss('#random').text()
expect(curRandom).toBe(initialRandom + '')
})
it('should show fallback before invalid JSON is returned from getStaticProps', async () => {
const html = await renderViaHTTP(next.url, '/non-json/foobar')
expect(html).toContain('"isFallback":true')
})
it('should not fallback before invalid JSON is returned from getStaticProps when blocking fallback', async () => {
const html = await renderViaHTTP(next.url, '/non-json-blocking/foobar')
expect(html).toContain('"isFallback":false')
})
it('should show error for invalid JSON returned from getStaticProps on SSR', async () => {
const browser = await webdriver(next.url, '/non-json/direct')
// FIXME: enable this
// expect(await getRedboxHeader(browser)).toMatch(
// /Error serializing `.time` returned from `getStaticProps`/
// )
// FIXME: disable this
Add hasRedbox fix (#60522) ## What? As @leerob and I found in-person when opening #57230 the `hasRedBox()` helper was incorrectly passing when it shouldn't pass in both the true and false case. This PR uses a different approach by waiting 7 seconds before checking, this leaves enough room for HMR / reloads to apply, it doesn't meaningfully slow down the test suite and increases reliability of the check as you can see below in the tests that were previously passing that are no longer passing. I've moved these to skipped tests for landing this PR as I want to avoid further issues being introduced while we fix them. @huozhi will investigate these next week 👍 Failing tests that are temporarily skipped: - https://github.com/vercel/next.js/pull/60522/files#diff-513b477050bf1a620697b4d16bc1e6850282cb54e0609bdc5fd34307bfa9e471R9 - https://github.com/vercel/next.js/pull/60522/files#diff-fa7d7c8c40914005c138d852eaf6a69ac0df51ec77bec548cbc5f0bfbdc8ebc5R25 - https://github.com/vercel/next.js/pull/60522/files#diff-6f9f7dc131416cb17938311939a56d8c0e685a8fe6e8fc0cf5cd04939c74f388R41 - https://github.com/vercel/next.js/pull/60522/files#diff-439830e340a320c56645e9d00aaf0fd0b492ddb90b6d7f9db89458ccc5158eb7R8 - https://github.com/vercel/next.js/pull/60522/files#diff-62938bf5cd4d84f96dde8b6bcb2c8e18099e6dfca269c4302229b79175c0250cR18 - https://github.com/vercel/next.js/pull/60522/files#diff-513b477050bf1a620697b4d16bc1e6850282cb54e0609bdc5fd34307bfa9e471R9 <!-- 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 # --> Closes NEXT-2059
2024-01-15 09:36:44 +01:00
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
/Failed to load static props/
)
})
it('should show error for invalid JSON returned from getStaticProps on CST', async () => {
const browser = await webdriver(next.url, '/')
await browser.elementByCss('#non-json').click()
// FIXME: enable this
// expect(await getRedboxHeader(browser)).toMatch(
// /Error serializing `.time` returned from `getStaticProps`/
// )
// FIXME: disable this
Add hasRedbox fix (#60522) ## What? As @leerob and I found in-person when opening #57230 the `hasRedBox()` helper was incorrectly passing when it shouldn't pass in both the true and false case. This PR uses a different approach by waiting 7 seconds before checking, this leaves enough room for HMR / reloads to apply, it doesn't meaningfully slow down the test suite and increases reliability of the check as you can see below in the tests that were previously passing that are no longer passing. I've moved these to skipped tests for landing this PR as I want to avoid further issues being introduced while we fix them. @huozhi will investigate these next week 👍 Failing tests that are temporarily skipped: - https://github.com/vercel/next.js/pull/60522/files#diff-513b477050bf1a620697b4d16bc1e6850282cb54e0609bdc5fd34307bfa9e471R9 - https://github.com/vercel/next.js/pull/60522/files#diff-fa7d7c8c40914005c138d852eaf6a69ac0df51ec77bec548cbc5f0bfbdc8ebc5R25 - https://github.com/vercel/next.js/pull/60522/files#diff-6f9f7dc131416cb17938311939a56d8c0e685a8fe6e8fc0cf5cd04939c74f388R41 - https://github.com/vercel/next.js/pull/60522/files#diff-439830e340a320c56645e9d00aaf0fd0b492ddb90b6d7f9db89458ccc5158eb7R8 - https://github.com/vercel/next.js/pull/60522/files#diff-62938bf5cd4d84f96dde8b6bcb2c8e18099e6dfca269c4302229b79175c0250cR18 - https://github.com/vercel/next.js/pull/60522/files#diff-513b477050bf1a620697b4d16bc1e6850282cb54e0609bdc5fd34307bfa9e471R9 <!-- 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 # --> Closes NEXT-2059
2024-01-15 09:36:44 +01:00
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(
/Failed to load static props/
)
})
it('should not contain headers already sent error', async () => {
await renderViaHTTP(next.url, '/fallback-only/some-fallback-post')
expect(next.cliOutput).not.toContain('ERR_HTTP_HEADERS_SENT')
})
} else {
it('should use correct caching headers for a no-revalidate page', async () => {
const initialRes = await fetchViaHTTP(next.url, '/something')
expect(initialRes.headers.get('cache-control')).toBe(
isDeploy
? 'public, max-age=0, must-revalidate'
: 's-maxage=31536000, stale-while-revalidate'
)
const initialHtml = await initialRes.text()
expect(initialHtml).toMatch(/hello.*?world/)
})
it('should not show error for invalid JSON returned from getStaticProps on SSR', async () => {
const browser = await webdriver(next.url, '/non-json/direct')
await check(() => getBrowserBodyText(browser), /hello /)
})
it('should not show error for invalid JSON returned from getStaticProps on CST', async () => {
const browser = await webdriver(next.url, '/')
await browser.elementByCss('#non-json').click()
await check(() => getBrowserBodyText(browser), /hello /)
})
if ((global as any).isNextStart && !isDeploy) {
it('outputs dataRoutes in routes-manifest correctly', async () => {
const { dataRoutes } = JSON.parse(
await next.readFile('.next/routes-manifest.json')
)
for (const route of dataRoutes) {
route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex)
}
expect(dataRoutes).toEqual([
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(next.buildId)}\\/index.json$`
),
page: '/',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/another.json$`
),
page: '/another',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/api\\-docs\\/(.+?)\\.json$`
),
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/api\\-docs/(?<nxtPslug>.+?)\\.json$`,
page: '/api-docs/[...slug]',
routeKeys: {
nxtPslug: 'nxtPslug',
},
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/bad-gssp.json$`
),
page: '/bad-gssp',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/bad-ssr.json$`
),
page: '/bad-ssr',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/blocking\\-fallback\\/([^\\/]+?)\\.json$`
),
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/blocking\\-fallback/(?<nxtPslug>[^/]+?)\\.json$`,
page: '/blocking-fallback/[slug]',
routeKeys: { nxtPslug: 'nxtPslug' },
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/blocking\\-fallback\\-once\\/([^\\/]+?)\\.json$`
),
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/blocking\\-fallback\\-once/(?<nxtPslug>[^/]+?)\\.json$`,
page: '/blocking-fallback-once/[slug]',
routeKeys: { nxtPslug: 'nxtPslug' },
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/blocking\\-fallback\\-some\\/([^\\/]+?)\\.json$`
),
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/blocking\\-fallback\\-some/(?<nxtPslug>[^/]+?)\\.json$`,
page: '/blocking-fallback-some/[slug]',
routeKeys: { nxtPslug: 'nxtPslug' },
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(next.buildId)}\\/blog.json$`
),
page: '/blog',
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/blog/(?<nxtPpost>[^/]+?)\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/blog\\/([^\\/]+?)\\.json$`
),
page: '/blog/[post]',
routeKeys: {
nxtPpost: 'nxtPpost',
},
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/blog/(?<nxtPpost>[^/]+?)/(?<nxtPcomment>[^/]+?)\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$`
),
page: '/blog/[post]/[comment]',
routeKeys: {
nxtPpost: 'nxtPpost',
nxtPcomment: 'nxtPcomment',
},
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/catchall/(?<nxtPslug>.+?)\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/catchall\\/(.+?)\\.json$`
),
page: '/catchall/[...slug]',
routeKeys: {
nxtPslug: 'nxtPslug',
},
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/catchall\\-explicit/(?<nxtPslug>.+?)\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/catchall\\-explicit\\/(.+?)\\.json$`
),
page: '/catchall-explicit/[...slug]',
routeKeys: {
nxtPslug: 'nxtPslug',
},
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/catchall\\-optional(?:/(?<nxtPslug>.+?))?\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/catchall\\-optional(?:\\/(.+?))?\\.json$`
),
page: '/catchall-optional/[[...slug]]',
routeKeys: {
nxtPslug: 'nxtPslug',
},
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/default-revalidate.json$`
),
page: '/default-revalidate',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/dynamic\\/([^\\/]+?)\\.json$`
),
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/dynamic/(?<nxtPslug>[^/]+?)\\.json$`,
page: '/dynamic/[slug]',
routeKeys: {
nxtPslug: 'nxtPslug',
},
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/fallback\\-only\\/([^\\/]+?)\\.json$`
),
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/fallback\\-only/(?<nxtPslug>[^/]+?)\\.json$`,
page: '/fallback-only/[slug]',
routeKeys: {
nxtPslug: 'nxtPslug',
},
},
// TODO: investigate index/index
// {
// dataRouteRegex: normalizeRegEx(
// `^\\/_next\\/data\\/${escapeRegex(
// next.buildId
// )}\\/index\\/index.json$`
// ),
// page: '/index',
// },
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/lang/(?<nxtPlang>[^/]+?)/about\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/lang\\/([^\\/]+?)\\/about\\.json$`
),
page: '/lang/[lang]/about',
routeKeys: {
nxtPlang: 'nxtPlang',
},
},
{
dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/large-page-data.json$`,
page: '/large-page-data',
},
{
dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/large-page-data-ssr.json$`,
page: '/large-page-data-ssr',
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/non\\-json/(?<nxtPp>[^/]+?)\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/non\\-json\\/([^\\/]+?)\\.json$`
),
page: '/non-json/[p]',
routeKeys: {
nxtPp: 'nxtPp',
},
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/non\\-json\\-blocking/(?<nxtPp>[^/]+?)\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/non\\-json\\-blocking\\/([^\\/]+?)\\.json$`
),
page: '/non-json-blocking/[p]',
routeKeys: {
nxtPp: 'nxtPp',
},
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/preview.json$`
),
page: '/preview',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/something.json$`
),
page: '/something',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(next.buildId)}\\/ssr.json$`
),
page: '/ssr',
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/user/(?<nxtPuser>[^/]+?)/profile\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/user\\/([^\\/]+?)\\/profile\\.json$`
),
page: '/user/[user]/profile',
routeKeys: {
nxtPuser: 'nxtPuser',
},
},
])
})
it('outputs a prerender-manifest correctly', async () => {
const manifest = JSON.parse(
await next.readFile('.next/prerender-manifest.json')
)
const escapedBuildId = escapeRegex(next.buildId)
Object.keys(manifest.dynamicRoutes).forEach((key) => {
const item = manifest.dynamicRoutes[key]
if (item.dataRouteRegex) {
item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex)
}
if (item.routeRegex) {
item.routeRegex = normalizeRegEx(item.routeRegex)
}
})
expect(manifest.version).toBe(4)
expect(manifest.routes).toEqual(expectedManifestRoutes())
expect(manifest.dynamicRoutes).toEqual({
'/api-docs/[...slug]': {
dataRoute: `/_next/data/${next.buildId}/api-docs/[...slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/api\\-docs\\/(.+?)\\.json$`
),
fallback: '/api-docs/[...slug].html',
routeRegex: normalizeRegEx(`^\\/api\\-docs\\/(.+?)(?:\\/)?$`),
},
'/blocking-fallback-once/[slug]': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback-once/[slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/blocking\\-fallback\\-once\\/([^\\/]+?)\\.json$`
),
fallback: null,
routeRegex: normalizeRegEx(
'^\\/blocking\\-fallback\\-once\\/([^\\/]+?)(?:\\/)?$'
),
},
'/blocking-fallback-some/[slug]': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback-some/[slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/blocking\\-fallback\\-some\\/([^\\/]+?)\\.json$`
),
fallback: null,
routeRegex: normalizeRegEx(
'^\\/blocking\\-fallback\\-some\\/([^\\/]+?)(?:\\/)?$'
),
},
'/blocking-fallback/[slug]': {
dataRoute: `/_next/data/${next.buildId}/blocking-fallback/[slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/blocking\\-fallback\\/([^\\/]+?)\\.json$`
),
fallback: null,
routeRegex: normalizeRegEx(
'^\\/blocking\\-fallback\\/([^\\/]+?)(?:\\/)?$'
),
},
'/blog/[post]': {
fallback: '/blog/[post].html',
dataRoute: `/_next/data/${next.buildId}/blog/[post].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/blog\\/([^\\/]+?)\\.json$`
),
routeRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)(?:\\/)?$'),
},
'/blog/[post]/[comment]': {
fallback: '/blog/[post]/[comment].html',
dataRoute: `/_next/data/${next.buildId}/blog/[post]/[comment].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$`
),
routeRegex: normalizeRegEx(
'^\\/blog\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$'
),
},
'/dynamic/[slug]': {
dataRoute: `/_next/data/${next.buildId}/dynamic/[slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/dynamic\\/([^\\/]+?)\\.json$`
),
fallback: false,
routeRegex: normalizeRegEx(`^\\/dynamic\\/([^\\/]+?)(?:\\/)?$`),
},
'/fallback-only/[slug]': {
dataRoute: `/_next/data/${next.buildId}/fallback-only/[slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/fallback\\-only\\/([^\\/]+?)\\.json$`
),
fallback: '/fallback-only/[slug].html',
routeRegex: normalizeRegEx(
'^\\/fallback\\-only\\/([^\\/]+?)(?:\\/)?$'
),
},
'/lang/[lang]/about': {
dataRoute: `/_next/data/${next.buildId}/lang/[lang]/about.json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/lang\\/([^\\/]+?)\\/about\\.json$`
),
fallback: false,
routeRegex: normalizeRegEx(
'^\\/lang\\/([^\\/]+?)\\/about(?:\\/)?$'
),
},
'/non-json-blocking/[p]': {
dataRoute: `/_next/data/${next.buildId}/non-json-blocking/[p].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/non\\-json\\-blocking\\/([^\\/]+?)\\.json$`
),
fallback: null,
routeRegex: normalizeRegEx(
'^\\/non\\-json\\-blocking\\/([^\\/]+?)(?:\\/)?$'
),
},
'/non-json/[p]': {
dataRoute: `/_next/data/${next.buildId}/non-json/[p].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/non\\-json\\/([^\\/]+?)\\.json$`
),
fallback: '/non-json/[p].html',
routeRegex: normalizeRegEx(
'^\\/non\\-json\\/([^\\/]+?)(?:\\/)?$'
),
},
'/user/[user]/profile': {
fallback: '/user/[user]/profile.html',
dataRoute: `/_next/data/${next.buildId}/user/[user]/profile.json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/user\\/([^\\/]+?)\\/profile\\.json$`
),
routeRegex: normalizeRegEx(
`^\\/user\\/([^\\/]+?)\\/profile(?:\\/)?$`
),
},
'/catchall/[...slug]': {
fallback: '/catchall/[...slug].html',
routeRegex: normalizeRegEx('^\\/catchall\\/(.+?)(?:\\/)?$'),
dataRoute: `/_next/data/${next.buildId}/catchall/[...slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/catchall\\/(.+?)\\.json$`
),
},
'/catchall-optional/[[...slug]]': {
dataRoute: `/_next/data/${next.buildId}/catchall-optional/[[...slug]].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/catchall\\-optional(?:\\/(.+?))?\\.json$`
),
fallback: false,
routeRegex: normalizeRegEx(
'^\\/catchall\\-optional(?:\\/(.+?))?(?:\\/)?$'
),
},
'/catchall-explicit/[...slug]': {
dataRoute: `/_next/data/${next.buildId}/catchall-explicit/[...slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/catchall\\-explicit\\/(.+?)\\.json$`
),
fallback: false,
routeRegex: normalizeRegEx(
'^\\/catchall\\-explicit\\/(.+?)(?:\\/)?$'
),
},
})
})
it('outputs prerendered files correctly', async () => {
const routes = [
'/another',
'/something',
'/blog/post-1',
'/blog/post-2/comment-2',
]
for (const route of routes) {
await next.readFile(join('.next/server/pages', `${route}.html`))
await next.readFile(join('.next/server/pages', `${route}.json`))
}
})
it('should handle de-duping correctly', async () => {
let vals = new Array(10).fill(null)
// use data route so we don't get the fallback
vals = await Promise.all(
vals.map(() =>
renderViaHTTP(
next.url,
`/_next/data/${next.buildId}/blog/post-10.json`
)
)
)
const val = vals[0]
expect(JSON.parse(val).pageProps.post).toBe('post-10')
expect(new Set(vals).size).toBe(1)
})
}
it('should not revalidate when set to false', async () => {
const route = '/something'
const initialHtml = await renderViaHTTP(next.url, route)
let newHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toBe(newHtml)
newHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toBe(newHtml)
newHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toBe(newHtml)
})
if (!isDeploy) {
// we can't guarantee cache time for deploy
it('should not revalidate when set to false in blocking fallback mode', async () => {
const route = '/blocking-fallback-once/test-no-revalidate'
const initialHtml = await renderViaHTTP(next.url, route)
let newHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toBe(newHtml)
newHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toBe(newHtml)
newHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toBe(newHtml)
})
}
it('should not throw error for on-demand revalidate for SSR path', async () => {
const res = await fetchViaHTTP(next.url, '/api/manual-revalidate', {
pathname: '/ssr',
})
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ revalidated: false })
expect(stripAnsi(next.cliOutput)).not.toContain('hasHeader')
})
it('should revalidate on-demand revalidate with preview cookie', async () => {
const initialRes = await fetchViaHTTP(next.url, '/preview')
expect(initialRes.status).toBe(200)
const initial$ = cheerio.load(await initialRes.text())
const initialProps = JSON.parse(initial$('#props').text())
expect(initialProps).toEqual({
preview: false,
previewData: null,
})
const previewRes = await fetchViaHTTP(next.url, '/api/enable')
let previewCookie = ''
expect(previewRes.headers.get('set-cookie')).toMatch(
/(__prerender_bypass|__next_preview_data)/
)
previewRes.headers
.get('set-cookie')
.split(',')
.forEach((s) => {
const c = cookie.parse(s)
const isBypass = c.__prerender_bypass
if (isBypass || c.__next_preview_data) {
if (previewCookie) previewCookie += '; '
previewCookie += `${
isBypass ? '__prerender_bypass' : '__next_preview_data'
}=${c[isBypass ? '__prerender_bypass' : '__next_preview_data']}`
}
})
const apiRes = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{ pathname: '/preview' },
{
headers: {
cookie: previewCookie,
},
}
)
expect(apiRes.status).toBe(200)
expect(await apiRes.json()).toEqual({ revalidated: true })
const postRevalidateRes = await fetchViaHTTP(next.url, '/preview')
expect(initialRes.status).toBe(200)
const postRevalidate$ = cheerio.load(await postRevalidateRes.text())
const postRevalidateProps = JSON.parse(postRevalidate$('#props').text())
expect(postRevalidateProps).toEqual({
preview: false,
previewData: null,
})
})
it('should handle revalidating HTML correctly', async () => {
const route = '/blog/post-2/comment-2'
const initialHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toMatch(/Post:.*?post-2/)
expect(initialHtml).toMatch(/Comment:.*?comment-2/)
let newHtml = await renderViaHTTP(next.url, route)
expect(newHtml).toMatch(/Post:.*?post-2/)
expect(newHtml).toMatch(/Comment:.*?comment-2/)
await waitFor(2 * 1000)
await renderViaHTTP(next.url, route)
await check(async () => {
newHtml = await renderViaHTTP(next.url, route)
return newHtml !== initialHtml ? 'success' : newHtml
}, 'success')
expect(newHtml === initialHtml).toBe(false)
expect(newHtml).toMatch(/Post:.*?post-2/)
expect(newHtml).toMatch(/Comment:.*?comment-2/)
})
it('should handle revalidating JSON correctly', async () => {
const route = `/_next/data/${next.buildId}/blog/post-2/comment-3.json`
const initialJson = await renderViaHTTP(next.url, route)
expect(initialJson).toMatch(/post-2/)
expect(initialJson).toMatch(/comment-3/)
let newJson = await renderViaHTTP(next.url, route)
if (!isDeploy) {
// we can't guarantee cache time on deploy
expect(newJson).toBe(initialJson)
}
await waitFor(2 * 1000)
await renderViaHTTP(next.url, route)
await check(async () => {
newJson = await renderViaHTTP(next.url, route)
return newJson !== initialJson ? 'success' : newJson
}, 'success')
expect(newJson === initialJson).toBe(false)
expect(newJson).toMatch(/post-2/)
expect(newJson).toMatch(/comment-3/)
})
it('should handle revalidating HTML correctly with blocking', async () => {
const route = '/blocking-fallback/pewpew'
const initialHtml = await renderViaHTTP(next.url, route)
expect(initialHtml).toMatch(/Post:.*?pewpew/)
let newHtml = await renderViaHTTP(next.url, route)
if (!isDeploy) {
// we can't guarantee the cache timing on deployment
expect(newHtml).toBe(initialHtml)
}
await waitFor(2 * 1000)
await renderViaHTTP(next.url, route)
await check(async () => {
newHtml = await renderViaHTTP(next.url, route)
return newHtml !== initialHtml ? 'success' : newHtml
}, 'success')
expect(newHtml === initialHtml).toBe(false)
expect(newHtml).toMatch(/Post:.*?pewpew/)
})
it('should handle revalidating JSON correctly with blocking', async () => {
const route = `/_next/data/${next.buildId}/blocking-fallback/pewpewdata.json`
const initialJson = await renderViaHTTP(next.url, route)
expect(initialJson).toMatch(/pewpewdata/)
let newJson = await renderViaHTTP(next.url, route)
if (!isDeploy) {
// we can't guarantee the cache on deploy
expect(newJson).toBe(initialJson)
}
await waitFor(2 * 1000)
await renderViaHTTP(next.url, route)
await check(async () => {
newJson = await renderViaHTTP(next.url, route)
return newJson !== initialJson ? 'success' : newJson
}, 'success')
expect(newJson === initialJson).toBe(false)
expect(newJson).toMatch(/pewpewdata/)
})
it('should handle revalidating HTML correctly with blocking and seed', async () => {
const route = '/blocking-fallback/a'
const initialHtml = await renderViaHTTP(next.url, route)
const $initial = cheerio.load(initialHtml)
expect($initial('p').text()).toBe('Post: a')
let newHtml = await renderViaHTTP(next.url, route)
if (!isDeploy) {
// we can't guarantee the cache time on deploy
expect(newHtml).toBe(initialHtml)
}
await waitFor(2 * 1000)
await renderViaHTTP(next.url, route)
await check(async () => {
newHtml = await renderViaHTTP(next.url, route)
return newHtml !== initialHtml ? 'success' : newHtml
}, 'success')
expect(newHtml === initialHtml).toBe(false)
const $new = cheerio.load(newHtml)
expect($new('p').text()).toBe('Post: a')
})
it('should handle revalidating JSON correctly with blocking and seed', async () => {
const route = `/_next/data/${next.buildId}/blocking-fallback/b.json`
const initialJson = await renderViaHTTP(next.url, route)
expect(JSON.parse(initialJson)).toMatchObject({
pageProps: { params: { slug: 'b' } },
})
let newJson = await renderViaHTTP(next.url, route)
if (!isDeploy) {
// we can't guarantee the cache time on deploy
expect(newJson).toBe(initialJson)
}
await waitFor(2 * 1000)
await renderViaHTTP(next.url, route)
await check(async () => {
newJson = await renderViaHTTP(next.url, route)
return newJson !== initialJson ? 'success' : newJson
}, 'success')
expect(newJson === initialJson).toBe(false)
expect(JSON.parse(newJson)).toMatchObject({
pageProps: { params: { slug: 'b' } },
})
})
it('should not fetch prerender data on mount', async () => {
const browser = await webdriver(next.url, '/blog/post-100')
await browser.eval('window.thisShouldStay = true')
await waitFor(2 * 1000)
const val = await browser.eval('window.thisShouldStay')
expect(val).toBe(true)
})
it('should not error when flushing cache files', async () => {
await fetchViaHTTP(next.url, '/user/user-1/profile')
await waitFor(500)
expect(next.cliOutput).not.toMatch(
/Failed to update prerender files for/
)
})
}
if ((global as any).isNextStart) {
it('should of formatted build output correctly', () => {
expect(next.cliOutput).toMatch(/○ \/normal/)
expect(next.cliOutput).toMatch(/● \/blog\/\[post\]/)
expect(next.cliOutput).toMatch(/\+2 more paths/)
})
it('should output traces', async () => {
const checks = [
{
page: '/_app',
tests: [
/webpack-runtime\.js/,
/node_modules\/react\/index\.js/,
/node_modules\/react\/package\.json/,
/node_modules\/react\/cjs\/react\.production\.min\.js/,
],
notTests: [],
},
{
page: '/another',
tests: [
/webpack-runtime\.js/,
/chunks\/.*?\.js/,
/node_modules\/react\/index\.js/,
/node_modules\/react\/package\.json/,
/node_modules\/react\/cjs\/react\.production\.min\.js/,
/\/world.txt/,
],
notTests: [
/node_modules\/@firebase\/firestore\/.*?\.js/,
/\/server\.js/,
],
},
{
page: '/blog/[post]',
tests: [
/webpack-runtime\.js/,
/chunks\/.*?\.js/,
/node_modules\/react\/index\.js/,
/node_modules\/react\/package\.json/,
/node_modules\/react\/cjs\/react\.production\.min\.js/,
/node_modules\/@firebase\/firestore\/.*?\.js/,
],
notTests: [/\/world.txt/],
},
]
for (const check of checks) {
const contents = await next.readFile(
join('.next/server/pages/', check.page + '.js.nft.json')
)
const { version, files } = JSON.parse(contents)
expect(version).toBe(1)
try {
expect(check.tests).toEqual(
expect.toSatisfyAll((item) =>
files.some((file) => item.test(file))
)
)
} catch (error) {
error.message += `\n\nFiles:\n${files.join('\n')}`
throw error
}
if (sep === '/') {
expect(
check.notTests.some((item) =>
files.some((file) => item.test(file))
)
).toBe(false)
}
}
})
}
if (!isDev) {
it('should handle on-demand revalidate for fallback: blocking', async () => {
const res = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-manual-1'
)
const html = await res.text()
const $ = cheerio.load(html)
const initialTime = $('#time').text()
const cacheHeader = isDeploy ? 'x-vercel-cache' : 'x-nextjs-cache'
expect(res.headers.get(cacheHeader)).toMatch(/MISS/)
expect($('p').text()).toMatch(/Post:.*?test-manual-1/)
if (!isDeploy) {
// we use retry here as the cache might still be
// writing to disk even after the above request has finished
await retry(async () => {
const res2 = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-manual-1'
)
const html2 = await res2.text()
const $2 = cheerio.load(html2)
expect(res2.headers.get(cacheHeader)).toMatch(/(HIT|STALE)/)
expect(initialTime).toBe($2('#time').text())
})
}
const res3 = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/blocking-fallback/test-manual-1',
},
{ redirect: 'manual' }
)
expect(res3.status).toBe(200)
const revalidateData = await res3.json()
expect(revalidateData.revalidated).toBe(true)
await retry(async () => {
const res4 = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-manual-1'
)
const html4 = await res4.text()
const $4 = cheerio.load(html4)
expect($4('#time').text()).not.toBe(initialTime)
expect(res4.headers.get(cacheHeader)).toMatch(/(HIT|STALE)/)
})
})
}
if (!isDev && !isDeploy) {
it('should automatically reset cache TTL when an error occurs and build cache was available', async () => {
await next.patchFile('error.txt', 'yes')
await waitFor(2000)
for (let i = 0; i < 5; i++) {
const res = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-errors-1'
)
expect(res.status).toBe(200)
}
await next.deleteFile('error.txt')
await check(
() =>
next.cliOutput.match(
/throwing error for \/blocking-fallback\/test-errors-1/
).length === 1
? 'success'
: next.cliOutput,
'success'
)
})
it('should automatically reset cache TTL when an error occurs and runtime cache was available', async () => {
const res = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-errors-2'
)
expect(res.status).toBe(200)
await waitFor(2000)
await next.patchFile('error.txt', 'yes')
for (let i = 0; i < 5; i++) {
const res = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-errors-2'
)
expect(res.status).toBe(200)
}
await next.deleteFile('error.txt')
await check(
() =>
next.cliOutput.match(
/throwing error for \/blocking-fallback\/test-errors-2/
).length === 1
? 'success'
: next.cliOutput,
'success'
)
})
it('should not on-demand revalidate for fallback: blocking with onlyGenerated if not generated', async () => {
const res = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/blocking-fallback/test-if-generated-1',
onlyGenerated: '1',
},
{ redirect: 'manual' }
)
expect(res.status).toBe(200)
const revalidateData = await res.json()
expect(revalidateData.revalidated).toBe(true)
expect(next.cliOutput).not.toContain(
`getStaticProps test-if-generated-1`
)
const res2 = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-if-generated-1'
)
expect(res2.headers.get('x-nextjs-cache')).toMatch(/(MISS)/)
expect(next.cliOutput).toContain(`getStaticProps test-if-generated-1`)
})
it('should on-demand revalidate for fallback: blocking with onlyGenerated if generated', async () => {
const beforeRevalidate = Date.now()
const res = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-if-generated-2'
)
await waitForCacheWrite(
'/blocking-fallback/test-if-generated-2',
beforeRevalidate
)
const html = await res.text()
const $ = cheerio.load(html)
const initialTime = $('#time').text()
expect($('p').text()).toMatch(/Post:.*?test-if-generated-2/)
expect(res.headers.get('x-nextjs-cache')).toMatch(/MISS/)
const res2 = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-if-generated-2'
)
const html2 = await res2.text()
const $2 = cheerio.load(html2)
expect(initialTime).toBe($2('#time').text())
expect(res2.headers.get('x-nextjs-cache')).toMatch(/(HIT|STALE)/)
const res3 = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/blocking-fallback/test-if-generated-2',
onlyGenerated: '1',
},
{ redirect: 'manual' }
)
expect(res3.status).toBe(200)
const revalidateData = await res3.json()
expect(revalidateData.revalidated).toBe(true)
const res4 = await fetchViaHTTP(
next.url,
'/blocking-fallback/test-if-generated-2'
)
const html4 = await res4.text()
const $4 = cheerio.load(html4)
expect($4('#time').text()).not.toBe(initialTime)
expect(res4.headers.get('x-nextjs-cache')).toMatch(/(HIT|STALE)/)
})
it('should on-demand revalidate for revalidate: false', async () => {
const html = await renderViaHTTP(
next.url,
'/blocking-fallback-once/test-manual-1'
)
const $ = cheerio.load(html)
const initialTime = $('#time').text()
expect($('p').text()).toMatch(/Post:.*?test-manual-1/)
const html2 = await renderViaHTTP(
next.url,
'/blocking-fallback-once/test-manual-1'
)
const $2 = cheerio.load(html2)
expect(initialTime).toBe($2('#time').text())
const res = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/blocking-fallback-once/test-manual-1',
},
{ redirect: 'manual' }
)
expect(res.status).toBe(200)
const revalidateData = await res.json()
expect(revalidateData.revalidated).toBe(true)
const html4 = await renderViaHTTP(
next.url,
'/blocking-fallback-once/test-manual-1'
)
const $4 = cheerio.load(html4)
expect($4('#time').text()).not.toBe(initialTime)
})
it('should on-demand revalidate that returns notFound: true', async () => {
const res = await fetchViaHTTP(
next.url,
'/blocking-fallback-once/404-on-manual-revalidate'
)
const html = await res.text()
const $ = cheerio.load(html)
const initialTime = $('#time').text()
expect(res.headers.get('x-nextjs-cache')).toBe('HIT')
expect($('p').text()).toMatch(/Post:.*?404-on-manual-revalidate/)
const html2 = await renderViaHTTP(
next.url,
'/blocking-fallback-once/404-on-manual-revalidate'
)
const $2 = cheerio.load(html2)
expect(initialTime).toBe($2('#time').text())
const res2 = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/blocking-fallback-once/404-on-manual-revalidate',
},
{ redirect: 'manual' }
)
expect(res2.status).toBe(200)
const revalidateData = await res2.json()
expect(revalidateData.revalidated).toBe(true)
const res3 = await fetchViaHTTP(
next.url,
'/blocking-fallback-once/404-on-manual-revalidate'
)
expect(res3.status).toBe(404)
expect(await res3.text()).toContain('This page could not be found')
expect(res3.headers.get('x-nextjs-cache')).toBe('HIT')
})
it('should handle on-demand revalidate for fallback: false', async () => {
const res = await fetchViaHTTP(
next.url,
'/catchall-explicit/test-manual-1'
)
expect(res.status).toBe(404)
// fallback: false pages should only manually revalidate
// prerendered paths
const res2 = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/catchall-explicity/test-manual-1',
},
{ redirect: 'manual' }
)
expect(res2.status).toBe(200)
const revalidateData = await res2.json()
expect(revalidateData.revalidated).toBe(false)
const res3 = await fetchViaHTTP(
next.url,
'/catchall-explicit/test-manual-1'
)
expect(res3.status).toBe(404)
const res4 = await fetchViaHTTP(next.url, '/catchall-explicit/first')
expect(res4.status).toBe(200)
const html = await res4.text()
const $ = cheerio.load(html)
const initialTime = $('#time').text()
const res5 = await fetchViaHTTP(
next.url,
'/api/manual-revalidate',
{
pathname: '/catchall-explicit/first',
},
{ redirect: 'manual' }
)
expect(res5.status).toBe(200)
expect((await res5.json()).revalidated).toBe(true)
const res6 = await fetchViaHTTP(next.url, '/catchall-explicit/first')
expect(res6.status).toBe(200)
const html2 = await res6.text()
const $2 = cheerio.load(html2)
expect(initialTime).not.toBe($2('#time').text())
})
}
it('should respond for catch-all deep folder', async () => {
const res = await fetchViaHTTP(
next.url,
`/_next/data/${next.buildId}/catchall/first/second/third.json`
)
expect(res.status).toBe(200)
expect(await res.text()).toContain('["first","second","third"]')
})
// this should come very last
it('should not fail to update incremental cache', async () => {
await waitFor(1000)
expect(next.cliOutput).not.toContain('Failed to update prerender cache')
})
it('should not have experimental undici warning', async () => {
await waitFor(1000)
expect(next.cliOutput).not.toContain('option is unnecessary in Node.js')
})
it('should not have attempted sending invalid payload', async () => {
expect(next.cliOutput).not.toContain('argument entity must be string')
})
}
runTests((global as any).isNextDev, (global as any).isNextDeploy)
})