import fs from 'fs-extra' import cheerio from 'cheerio' import { join, sep } from 'path' import escapeRegex from 'escape-string-regexp' import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' import { check, fetchViaHTTP, getBrowserBodyText, getRedboxHeader, hasRedbox, normalizeRegEx, renderViaHTTP, waitFor, } from 'next-test-utils' import webdriver from 'next-webdriver' 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, }, '/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/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() if (isDev) { expect(snapTime).not.toMatch(nextTime) } else { expect(snapTime).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') 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}`, }) }) if ((global as any).isNextDev) { 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 128 kB, this amount of data can reduce performance/ ) }) 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 }) =>
url: {url}
` ) 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) }) it('should log error in console and browser in dev 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('{/*[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/non\\-json\\/([^\\/]+?)\\.json$` ), page: '/non-json/[p]', routeKeys: { p: 'p', }, }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/non\\-json\\-blocking/(?
[^/]+?)\\.json$`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/non\\-json\\-blocking\\/([^\\/]+?)\\.json$`
),
page: '/non-json-blocking/[p]',
routeKeys: {
p: 'p',
},
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
next.buildId
)}\\/something.json$`
),
page: '/something',
},
{
namedDataRouteRegex: `^/_next/data/${escapeRegex(
next.buildId
)}/user/(?