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 'test/lib/next-modes/base' import { check, fetchViaHTTP, getBrowserBodyText, getRedboxHeader, hasRedbox, normalizeRegEx, renderViaHTTP, 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') 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 }) =>

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('{/* */}', '') ) await browser.waitForElementByCss('#after-change') // we need to reload the page to trigger getStaticProps await browser.refresh() expect(await hasRedbox(browser, true)).toBe(true) const errOverlayContent = await getRedboxHeader(browser) await next.patchFile(indexPage, origContent) const errorMsg = /oops from getStaticProps/ expect(next.cliOutput).toMatch(errorMsg) expect(errOverlayContent).toMatch(errorMsg) }) 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 expect(await hasRedbox(browser, true)).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 expect(await hasRedbox(browser, true)).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/(?.+?)\\.json$`, page: '/api-docs/[...slug]', routeKeys: { slug: 'slug', }, }, { 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/(?[^/]+?)\\.json$`, page: '/blocking-fallback/[slug]', routeKeys: { slug: 'slug' }, }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/blocking\\-fallback\\-once\\/([^\\/]+?)\\.json$` ), namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/blocking\\-fallback\\-once/(?[^/]+?)\\.json$`, page: '/blocking-fallback-once/[slug]', routeKeys: { slug: 'slug' }, }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/blocking\\-fallback\\-some\\/([^\\/]+?)\\.json$` ), namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/blocking\\-fallback\\-some/(?[^/]+?)\\.json$`, page: '/blocking-fallback-some/[slug]', routeKeys: { slug: 'slug' }, }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(next.buildId)}\\/blog.json$` ), page: '/blog', }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/blog/(?[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/blog\\/([^\\/]+?)\\.json$` ), page: '/blog/[post]', routeKeys: { post: 'post', }, }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/blog/(?[^/]+?)/(?[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$` ), page: '/blog/[post]/[comment]', routeKeys: { post: 'post', comment: 'comment', }, }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/catchall/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/catchall\\/(.+?)\\.json$` ), page: '/catchall/[...slug]', routeKeys: { slug: 'slug', }, }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/catchall\\-explicit/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/catchall\\-explicit\\/(.+?)\\.json$` ), page: '/catchall-explicit/[...slug]', routeKeys: { slug: 'slug', }, }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/catchall\\-optional(?:/(?.+?))?\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/catchall\\-optional(?:\\/(.+?))?\\.json$` ), page: '/catchall-optional/[[...slug]]', routeKeys: { slug: 'slug', }, }, { 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/(?[^/]+?)\\.json$`, page: '/dynamic/[slug]', routeKeys: { slug: 'slug', }, }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/fallback\\-only\\/([^\\/]+?)\\.json$` ), namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/fallback\\-only/(?[^/]+?)\\.json$`, page: '/fallback-only/[slug]', routeKeys: { slug: 'slug', }, }, // TODO: investigate index/index // { // dataRouteRegex: normalizeRegEx( // `^\\/_next\\/data\\/${escapeRegex( // next.buildId // )}\\/index\\/index.json$` // ), // page: '/index', // }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( next.buildId )}/lang/(?[^/]+?)/about\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/lang\\/([^\\/]+?)\\/about\\.json$` ), page: '/lang/[lang]/about', routeKeys: { lang: 'lang', }, }, { 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/(?

[^/]+?)\\.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 )}\\/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/(?[^/]+?)/profile\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( next.buildId )}\\/user\\/([^\\/]+?)\\/profile\\.json$` ), page: '/user/[user]/profile', routeKeys: { user: 'user', }, }, ]) }) 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 manual 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 manual 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((c) => { c = cookie.parse(c) 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/, /node_modules\/next/, ], 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/, /node_modules\/next/, /\/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\/next/, /next\/router\.js/, /next\/dist\/client\/router\.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) console.log( check.tests.map((item) => files.some((file) => item.test(file))) ) expect( check.tests.every((item) => files.some((file) => item.test(file))) ).toBe(true) if (sep === '/') { expect( check.notTests.some((item) => files.some((file) => item.test(file)) ) ).toBe(false) } } }) } if (!isDev) { it('should handle manual revalidate for fallback: blocking', async () => { const beforeRevalidate = Date.now() const res = await fetchViaHTTP( next.url, '/blocking-fallback/test-manual-1' ) if (!isDeploy) { await waitForCacheWrite( '/blocking-fallback/test-manual-1', beforeRevalidate ) } 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) { 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 check(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)/) return 'success' }, 'success') }) } 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 manual 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 manual 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(res.headers.get('x-nextjs-cache')).toMatch(/MISS/) expect($('p').text()).toMatch(/Post:.*?test-if-generated-2/) const res2 = await fetchViaHTTP( next.url, '/blocking-fallback/test-if-generated-2' ) const html2 = await res2.text() const $2 = cheerio.load(html2) expect(res2.headers.get('x-nextjs-cache')).toMatch(/(HIT|STALE)/) expect(initialTime).toBe($2('#time').text()) 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 manual 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 manual 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 manual 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) })