import { createNext, FileRef } from 'e2e-utils'
import crypto from 'crypto'
import { NextInstance } from 'test/lib/next-modes/base'
import { check, fetchViaHTTP, getRedboxHeader, hasRedbox, renderViaHTTP, waitFor } from 'next-test-utils'
import path from 'path'
import cheerio from 'cheerio'
import webdriver from 'next-webdriver'
describe('app dir', () => {
const isDev = (global as any).isNextDev
let next: NextInstance
function runTests() {
beforeAll(async () => {
next = await createNext({
files: new FileRef(path.join(__dirname, 'app')),
dependencies: {
swr: '2.0.0-rc.0',
react: 'latest',
'react-dom': 'latest',
sass: 'latest',
},
})
}, 600000)
afterAll(async () => {
await next.destroy()
})
if (!(global as any).isNextDeploy) {
it('should not share edge workers', async () => {
const controller1 = new AbortController()
const controller2 = new AbortController()
fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, {
signal: controller1.signal,
}).catch(() => {})
fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, {
signal: controller2.signal,
}).catch(() => {})
await waitFor(1000)
controller1.abort()
const controller3 = new AbortController()
fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, {
signal: controller3.signal,
}).catch(() => {})
await waitFor(1000)
controller2.abort()
controller3.abort()
const res = await fetchViaHTTP(next.url, '/slow-page-no-loading')
expect(res.status).toBe(200)
expect(await res.text()).toContain('hello from slow page')
expect(next.cliOutput).not.toContain('A separate worker must be used for each render')
})
}
if ((global as any).isNextStart) {
it('should generate build traces correctly', async () => {
const trace = JSON.parse(
await next.readFile('.next/server/app/dashboard/deployments/[id]/page.js.nft.json'),
) as { files: string[] }
expect(trace.files.some((file) => file.endsWith('data.json'))).toBe(true)
})
}
it('should use application/octet-stream for flight', async () => {
const res = await fetchViaHTTP(
next.url,
'/dashboard/deployments/123',
{},
{
headers: {
['RSC'.toString()]: '1',
},
},
)
expect(res.headers.get('Content-Type')).toBe('application/octet-stream')
})
it('should use application/octet-stream for flight with edge runtime', async () => {
const res = await fetchViaHTTP(
next.url,
'/dashboard',
{},
{
headers: {
['RSC'.toString()]: '1',
},
},
)
expect(res.headers.get('Content-Type')).toBe('application/octet-stream')
})
it('should pass props from getServerSideProps in root layout', async () => {
const html = await renderViaHTTP(next.url, '/dashboard')
const $ = cheerio.load(html)
expect($('title').text()).toBe('hello world')
})
it('should serve from pages', async () => {
const html = await renderViaHTTP(next.url, '/')
expect(html).toContain('hello from pages/index')
// esm imports should work fine in pages/
expect(html).toContain('swr-index')
})
it('should serve dynamic route from pages', async () => {
const html = await renderViaHTTP(next.url, '/blog/first')
expect(html).toContain('hello from pages/blog/[slug]')
})
it('should serve from public', async () => {
const html = await renderViaHTTP(next.url, '/hello.txt')
expect(html).toContain('hello world')
})
it('should serve from app', async () => {
const html = await renderViaHTTP(next.url, '/dashboard')
expect(html).toContain('hello from app/dashboard')
})
if (!(global as any).isNextDeploy) {
it('should serve /index as separate page', async () => {
const html = await renderViaHTTP(next.url, '/dashboard/index')
expect(html).toContain('hello from app/dashboard/index')
// should load chunks generated via async import correctly with React.lazy
expect(html).toContain('hello from lazy')
// should support `dynamic` in both server and client components
expect(html).toContain('hello from dynamic on server')
expect(html).toContain('hello from dynamic on client')
})
it('should serve polyfills for browsers that do not support modules', async () => {
const html = await renderViaHTTP(next.url, '/dashboard/index')
expect(html).toMatch(/"'`)
expect(res.status).toBe(500)
})
})
describe('template component', () => {
it('should render the template that holds state in a client component and reset on navigation', async () => {
const browser = await webdriver(next.url, '/template/clientcomponent')
expect(await browser.elementByCss('h1').text()).toBe('Template 0')
await browser.elementByCss('button').click()
expect(await browser.elementByCss('h1').text()).toBe('Template 1')
await browser.elementByCss('#link').click()
await browser.waitForElementByCss('#other-page')
expect(await browser.elementByCss('h1').text()).toBe('Template 0')
await browser.elementByCss('button').click()
expect(await browser.elementByCss('h1').text()).toBe('Template 1')
await browser.elementByCss('#link').click()
await browser.waitForElementByCss('#page')
expect(await browser.elementByCss('h1').text()).toBe('Template 0')
})
// TODO-APP: disable failing test and investigate later
;(isDev ? it.skip : it)(
'should render the template that is a server component and rerender on navigation',
async () => {
const browser = await webdriver(next.url, '/template/servercomponent')
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('h1').text()).toStartWith('Template')
const currentTime = await browser.elementByCss('#performance-now').text()
await browser.elementByCss('#link').click()
await browser.waitForElementByCss('#other-page')
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('h1').text()).toStartWith('Template')
// template should rerender on navigation even when it's a server component
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('#performance-now').text()).toBe(currentTime)
await browser.elementByCss('#link').click()
await browser.waitForElementByCss('#page')
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('#performance-now').text()).toBe(currentTime)
},
)
})
describe('error component', () => {
it('should trigger error component when an error happens during rendering', async () => {
const browser = await webdriver(next.url, '/error/client-component')
await browser.elementByCss('#error-trigger-button').click()
if (isDev) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
await browser
expect(
await browser.waitForElementByCss('#error-boundary-message').elementByCss('#error-boundary-message').text(),
).toBe('An error occurred: this is a test')
}
})
it('should trigger error component when an error happens during server components rendering', async () => {
const browser = await webdriver(next.url, '/error/server-component')
if (isDev) {
expect(
await browser.waitForElementByCss('#error-boundary-message').elementByCss('#error-boundary-message').text(),
).toBe('this is a test')
expect(
await browser.waitForElementByCss('#error-boundary-digest').text(),
// Digest of the error message should be stable.
).not.toBe('')
// TODO-APP: ensure error overlay is shown for errors that happened before/during hydration
// expect(await hasRedbox(browser)).toBe(true)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
await browser
expect(await browser.waitForElementByCss('#error-boundary-message').text()).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.',
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text(),
// Digest of the error message should be stable.
).not.toBe('')
}
})
it('should use default error boundary for prod and overlay for dev when no error component specified', async () => {
const browser = await webdriver(next.url, '/error/global-error-boundary')
await browser.elementByCss('#error-trigger-button').click()
// .waitForElementByCss('body')
if (isDev) {
expect(await hasRedbox(browser)).toBe(true)
console.log('getRedboxHeader', await getRedboxHeader(browser))
// expect(await getRedboxHeader(browser)).toMatch(/An error occurred: this is a test/)
} else {
expect(await browser.waitForElementByCss('body').elementByCss('body').text()).toBe(
'Application error: a client-side exception has occurred (see the browser console for more information).',
)
}
})
if (!isDev) {
it('should allow resetting error boundary', async () => {
const browser = await webdriver(next.url, '/error/client-component')
// Try triggering and resetting a few times in a row
for (let i = 0; i < 5; i++) {
await browser.elementByCss('#error-trigger-button').click().waitForElementByCss('#error-boundary-message')
expect(await browser.elementByCss('#error-boundary-message').text()).toBe(
'An error occurred: this is a test',
)
await browser.elementByCss('#reset').click().waitForElementByCss('#error-trigger-button')
expect(await browser.elementByCss('#error-trigger-button').text()).toBe('Trigger Error!')
}
})
it('should hydrate empty shell to handle server-side rendering errors', async () => {
const browser = await webdriver(next.url, '/error/ssr-error-client-component')
const logs = await browser.log()
const errors = logs
.filter((x) => x.source === 'error')
.map((x) => x.message)
.join('\n')
expect(errors).toInclude('Error during SSR')
})
}
})
describe('known bugs', () => {
describe('should support React cache', () => {
it('server component', async () => {
const browser = await webdriver(next.url, '/react-cache/server-component')
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
it('server component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-cache')
await browser.elementByCss('#to-server-component').click().waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
it('client component', async () => {
const browser = await webdriver(next.url, '/react-cache/client-component')
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
it('client component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-cache')
await browser.elementByCss('#to-client-component').click().waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
})
describe('should support React fetch instrumentation', () => {
it('server component', async () => {
const browser = await webdriver(next.url, '/react-fetch/server-component')
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
it('server component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-fetch')
await browser.elementByCss('#to-server-component').click().waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
// TODO-APP: React doesn't have fetch deduping for client components yet.
it.skip('client component', async () => {
const browser = await webdriver(next.url, '/react-fetch/client-component')
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
// TODO-APP: React doesn't have fetch deduping for client components yet.
it.skip('client component client-navigation', async () => {
const browser = await webdriver(next.url, '/react-fetch')
await browser.elementByCss('#to-client-component').click().waitForElementByCss('#value-1', 10000)
const val1 = await browser.elementByCss('#value-1').text()
const val2 = await browser.elementByCss('#value-2').text()
expect(val1).toBe(val2)
})
})
it('should not share flight data between requests', async () => {
const fetches = await Promise.all(
[...new Array(5)].map(() => renderViaHTTP(next.url, '/loading-bug/electronics')),
)
for (const text of fetches) {
const $ = cheerio.load(text)
expect($('#category-id').text()).toBe('electronicsabc')
}
})
it('should handle as on next/link', async () => {
const browser = await webdriver(next.url, '/link-with-as')
expect(await browser.elementByCss('#link-to-info-123').click().waitForElementByCss('#message').text()).toBe(
`hello from app/dashboard/deployments/info/[id]. ID is: 123`,
)
})
it('should handle next/link back to initially loaded page', async () => {
const browser = await webdriver(next.url, '/linking/about')
expect(await browser.elementByCss('a[href="/linking"]').click().waitForElementByCss('#home-page').text()).toBe(
`Home page`,
)
expect(
await browser.elementByCss('a[href="/linking/about"]').click().waitForElementByCss('#about-page').text(),
).toBe(`About page`)
})
it('should not do additional pushState when already on the page', async () => {
const browser = await webdriver(next.url, '/linking/about')
const goToLinkingPage = async () => {
expect(
await browser.elementByCss('a[href="/linking"]').click().waitForElementByCss('#home-page').text(),
).toBe(`Home page`)
}
await goToLinkingPage()
await waitFor(1000)
await goToLinkingPage()
await waitFor(1000)
await goToLinkingPage()
await waitFor(1000)
expect(await browser.back().waitForElementByCss('#about-page', 2000).text()).toBe(`About page`)
})
})
describe('not-found', () => {
it('should trigger not-found in a server component', async () => {
const browser = await webdriver(next.url, '/not-found/servercomponent')
expect(await browser.waitForElementByCss('#not-found-component').text()).toBe('Not Found!')
expect(await browser.waitForElementByCss('meta[name="robots"]').getAttribute('content')).toBe('noindex')
})
it('should trigger not-found in a client component', async () => {
const browser = await webdriver(next.url, '/not-found/clientcomponent')
expect(await browser.waitForElementByCss('#not-found-component').text()).toBe('Not Found!')
expect(await browser.waitForElementByCss('meta[name="robots"]').getAttribute('content')).toBe('noindex')
})
it('should trigger not-found client-side', async () => {
const browser = await webdriver(next.url, '/not-found/client-side')
await browser.elementByCss('button').click().waitForElementByCss('#not-found-component')
expect(await browser.elementByCss('#not-found-component').text()).toBe('Not Found!')
expect(await browser.waitForElementByCss('meta[name="robots"]').getAttribute('content')).toBe('noindex')
})
})
describe('bots', () => {
if (!(global as any).isNextDeploy) {
it('should block rendering for bots and return 404 status', async () => {
const res = await fetchViaHTTP(next.url, '/not-found/servercomponent', '', {
headers: {
'User-Agent': 'Googlebot',
},
})
expect(res.status).toBe(404)
expect(await res.text()).toInclude('"noindex"')
})
}
})
describe('redirect', () => {
describe('components', () => {
it('should redirect in a server component', async () => {
const browser = await webdriver(next.url, '/redirect/servercomponent')
await browser.waitForElementByCss('#result-page')
expect(await browser.elementByCss('#result-page').text()).toBe('Result Page')
})
it('should redirect in a client component', async () => {
const browser = await webdriver(next.url, '/redirect/clientcomponent')
await browser.waitForElementByCss('#result-page')
expect(await browser.elementByCss('#result-page').text()).toBe('Result Page')
})
// TODO-APP: Enable in development
it('should redirect client-side', async () => {
const browser = await webdriver(next.url, '/redirect/client-side')
await browser.elementByCss('button').click().waitForElementByCss('#result-page')
// eslint-disable-next-line jest/no-standalone-expect
expect(await browser.elementByCss('#result-page').text()).toBe('Result Page')
})
})
describe('next.config.js redirects', () => {
it('should redirect from next.config.js', async () => {
const browser = await webdriver(next.url, '/redirect/a')
expect(await browser.elementByCss('h1').text()).toBe('Dashboard')
expect(await browser.url()).toBe(next.url + '/dashboard')
})
it('should redirect from next.config.js with link navigation', async () => {
const browser = await webdriver(next.url, '/redirect/next-config-redirect')
await browser.elementByCss('#redirect-a').click().waitForElementByCss('h1')
expect(await browser.elementByCss('h1').text()).toBe('Dashboard')
expect(await browser.url()).toBe(next.url + '/dashboard')
})
})
describe('middleware redirects', () => {
it('should redirect from middleware', async () => {
const browser = await webdriver(next.url, '/redirect-middleware-to-dashboard')
expect(await browser.elementByCss('h1').text()).toBe('Dashboard')
expect(await browser.url()).toBe(next.url + '/dashboard')
})
it('should redirect from middleware with link navigation', async () => {
const browser = await webdriver(next.url, '/redirect/next-middleware-redirect')
await browser.elementByCss('#redirect-middleware').click().waitForElementByCss('h1')
expect(await browser.elementByCss('h1').text()).toBe('Dashboard')
expect(await browser.url()).toBe(next.url + '/dashboard')
})
})
})
describe('nested navigation', () => {
it('should navigate to nested pages', async () => {
const browser = await webdriver(next.url, '/nested-navigation')
expect(await browser.elementByCss('h1').text()).toBe('Home')
const pages = [
['Electronics', ['Phones', 'Tablets', 'Laptops']],
['Clothing', ['Tops', 'Shorts', 'Shoes']],
['Books', ['Fiction', 'Biography', 'Education']],
] as const
for (const [category, subCategories] of pages) {
expect(
await browser
.elementByCss(`a[href="/nested-navigation/${category.toLowerCase()}"]`)
.click()
.waitForElementByCss(`#all-${category.toLowerCase()}`)
.text(),
).toBe(`All ${category}`)
for (const subcategory of subCategories) {
expect(
await browser
.elementByCss(`a[href="/nested-navigation/${category.toLowerCase()}/${subcategory.toLowerCase()}"]`)
.click()
.waitForElementByCss(`#${subcategory.toLowerCase()}`)
.text(),
).toBe(`${subcategory}`)
}
}
})
})
describe('next/script', () => {
if (!(global as any).isNextDeploy) {
it('should support next/script and render in correct order', async () => {
const browser = await webdriver(next.url, '/script')
// Wait for lazyOnload scripts to be ready.
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await browser.eval(`window._script_order`)).toStrictEqual([1, 1.5, 2, 2.5, 'render', 3, 4])
})
}
it('should insert preload tags for beforeInteractive and afterInteractive scripts', async () => {
const html = await renderViaHTTP(next.url, '/script')
expect(html).toContain('')
expect(html).toContain('')
expect(html).toContain('')
// test4.js has lazyOnload which doesn't need to be preloaded
expect(html).not.toContain('')
})
})
describe('data fetch with response over 16KB with chunked encoding', () => {
it('should load page when fetching a large amount of data', async () => {
const browser = await webdriver(next.url, '/very-large-data-fetch')
expect(await (await browser.waitForElementByCss('#done')).text()).toBe('Hello world')
expect(await browser.elementByCss('p').text()).toBe('item count 128000')
})
})
}
runTests()
})