import { nextTestSetup, isNextStart } from 'e2e-utils'
import { splitResponseWithPPRSentinel } from 'e2e-utils/ppr'
import { links } from './components/links'
import cheerio from 'cheerio'
import { getCacheHeader, retry } from 'next-test-utils'
import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param'
type Page = {
pathname: string
dynamic: boolean | 'force-dynamic' | 'force-static'
revalidate?: number
/**
* If true, this indicates that the test case should not expect any content
* to be sent as the static part.
*/
emptyStaticPart?: boolean
fallback?: boolean
}
const pages: Page[] = [
{ pathname: '/', dynamic: true },
{ pathname: '/nested/a', dynamic: true, revalidate: 120 },
{ pathname: '/nested/b', dynamic: true, revalidate: 120 },
{ pathname: '/nested/c', dynamic: true, revalidate: 120 },
{ pathname: '/metadata', dynamic: true, revalidate: 120 },
{ pathname: '/on-demand/a', dynamic: true },
{ pathname: '/on-demand/b', dynamic: true },
{ pathname: '/on-demand/c', dynamic: true },
{ pathname: '/loading/a', dynamic: true, revalidate: 120 },
{ pathname: '/loading/b', dynamic: true, revalidate: 120 },
{ pathname: '/loading/c', dynamic: true, revalidate: 120 },
{ pathname: '/static', dynamic: false },
{ pathname: '/no-suspense', dynamic: true, emptyStaticPart: true },
{ pathname: '/no-suspense/nested/a', dynamic: true, emptyStaticPart: true },
{ pathname: '/no-suspense/nested/b', dynamic: true, emptyStaticPart: true },
{ pathname: '/no-suspense/nested/c', dynamic: true, emptyStaticPart: true },
{ pathname: '/dynamic/force-dynamic', dynamic: 'force-dynamic' },
{ pathname: '/dynamic/force-dynamic/nested/a', dynamic: 'force-dynamic' },
{ pathname: '/dynamic/force-dynamic/nested/b', dynamic: 'force-dynamic' },
{ pathname: '/dynamic/force-dynamic/nested/c', dynamic: 'force-dynamic' },
{
pathname: '/dynamic/force-static',
dynamic: 'force-static',
revalidate: 120,
},
]
const addCacheBustingSearchParam = async (
pathname: string,
headers: Record<string, string | string[] | undefined>
) => {
const cacheKey = await computeCacheBustingSearchParam(
headers['next-router-prefetch'] ? '1' : '0',
headers['next-router-segment-prefetch'],
headers['next-router-state-tree'],
headers['next-url']
)
if (cacheKey.length === 0) {
return pathname
}
const url = new URL(pathname, 'http://localhost')
url.searchParams.set('_rsc', cacheKey)
return url.pathname + url.search
}
/**
* Expects that the cache-control header contains the given directives in any
* order.
*
* @param header The cache-control header to check.
* @param directives The directives to expect.
*/
const expectDirectives = (header: string, directives: string[]) => {
const split = header.split(',').map((directive) => directive.trim())
for (const directive of directives) {
expect(split).toContain(directive)
}
expect(split.length).toEqual(directives.length)
}
// TODO(NAR-423): Migrate to Cache Components.
describe.skip('ppr-full', () => {
const { next, isNextDev, isNextDeploy } = nextTestSetup({
files: __dirname,
})
describe('Test Setup', () => {
it('has all the test pathnames listed in the links component', () => {
for (const { pathname } of pages) {
expect(links).toContainEqual(
expect.objectContaining({ href: pathname })
)
}
})
})
describe('Metadata', () => {
it('should set the right metadata when generateMetadata uses dynamic APIs', async () => {
const browser = await next.browser('/metadata')
try {
const title = await browser.elementByCss('title').text()
expect(title).toEqual('Metadata')
} finally {
await browser.close()
}
})
})
describe('HTML Response', () => {
describe.each(pages)(
'for $pathname',
({ pathname, dynamic, revalidate, emptyStaticPart }) => {
beforeAll(async () => {
// Hit the page once to populate the cache.
const res = await next.fetch(pathname)
// Consume the response body to ensure the cache is populated.
await res.text()
})
it('should allow soft navigations to and from the / page', async () => {
const browser = await next.browser('/')
await browser.waitForElementByCss(`[data-pathname="/"]`)
// Add a window var so we can detect if there was a full navigation.
const now = Date.now()
await browser.eval(`window.beforeNav = ${now}`)
// Navigate to the page and wait for the page to load.
await browser.elementByCss(`a[href="${pathname}"]`).click()
await browser.waitForElementByCss(`[data-pathname="${pathname}"]`)
// Ensure we did a client navigation and not a full page navigation.
let beforeNav = await browser.eval('window.beforeNav')
expect(beforeNav).toBe(now)
// Navigate back to the home page and wait for the page to load.
await browser.elementByCss(`a[href="/"]`).click()
await browser.waitForElementByCss(`[data-pathname="/"]`)
// Ensure we did a client navigation and not a full page navigation.
beforeNav = await browser.eval('window.beforeNav')
expect(beforeNav).toBe(now)
})
it('should allow navigations to and from a pages/ page', async () => {
const browser = await next.browser(pathname)
try {
await browser.waitForElementByCss(`[data-pathname="${pathname}"]`)
// Add a window var so we can detect if there was a full navigation.
const now = Date.now()
await browser.eval(`window.beforeNav = ${now.toString()}`)
// Navigate to the pages page and wait for the page to load.
await browser.elementByCss(`a[href="/pages"]`).click()
await browser.waitForElementByCss('[data-pathname="/pages"]')
// Ensure we did a full page navigation, and not a client navigation.
let beforeNav = await browser.eval('window.beforeNav')
expect(beforeNav).not.toBe(now)
await browser.eval(`window.beforeNav = ${now.toString()}`)
// Navigate back and wait for the page to load.
await browser.elementByCss(`a[href="${pathname}"]`).click()
await browser.waitForElementByCss(`[data-pathname="${pathname}"]`)
// Ensure we did a full page navigation, and not a client navigation.
beforeNav = await browser.eval('window.beforeNav')
expect(beforeNav).not.toBe(now)
} finally {
await browser.close()
}
})
it('should have correct headers', async () => {
const res = await next.fetch(pathname)
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual(
'text/html; charset=utf-8'
)
const cacheControl = res.headers.get('cache-control')
if (isNextDeploy) {
expect(cacheControl).toEqual('public, max-age=0, must-revalidate')
} else if (isNextDev) {
expect(cacheControl).toEqual('no-cache, must-revalidate')
} else if (dynamic === false || dynamic === 'force-static') {
expect(cacheControl).toEqual(
revalidate === undefined
? `s-maxage=31536000`
: `s-maxage=${revalidate}, stale-while-revalidate=${31536000 - revalidate}`
)
} else {
expect(cacheControl).toEqual(
'private, no-cache, no-store, max-age=0, must-revalidate'
)
}
// The cache header is not relevant in development and is not
// deterministic enough for this table test to verify.
if (isNextDev) return
if (
!isNextDeploy &&
(dynamic === false || dynamic === 'force-static')
) {
expect(res.headers.get('x-nextjs-cache')).toEqual('HIT')
} else {
expect(res.headers.get('x-nextjs-cache')).toEqual(null)
}
})
if (dynamic === true && !isNextDev && !isNextDeploy) {
it('should cache the static part', async () => {
const dynamicValue = `${Date.now()}:${Math.random()}`
const [staticPart, dynamicPart] =
await splitResponseWithPPRSentinel(async () => {
const res = await next.fetch(pathname, {
headers: {
'X-Test-Input': dynamicValue,
},
})
expect(res.status).toBe(200)
return res.body
})
// The dynamic part should contain the dynamic input.
expect(dynamicPart).toContain(dynamicValue)
// The static part should not contain the dynamic input.
if (emptyStaticPart) {
expect(staticPart).toBe('')
} else {
expect(staticPart).toContain('Dynamic Loading...')
expect(staticPart).not.toContain(dynamicValue)
}
})
}
if (dynamic === true || dynamic === 'force-dynamic') {
it('should resume with dynamic content', async () => {
const expected = `${Date.now()}:${Math.random()}`
const res = await next.fetch(pathname, {
headers: { 'X-Test-Input': expected },
})
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual(
'text/html; charset=utf-8'
)
const html = await res.text()
expect(html).toContain(expected)
expect(html).not.toContain('MISSING:USER-AGENT')
expect(html).toContain('</html>')
})
} else {
it('should not contain dynamic content', async () => {
const unexpected = `${Date.now()}:${Math.random()}`
const res = await next.fetch(pathname, {
headers: { 'X-Test-Input': unexpected },
})
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual(
'text/html; charset=utf-8'
)
const html = await res.text()
expect(html).not.toContain(unexpected)
if (dynamic !== false) {
expect(html).toContain('MISSING:USER-AGENT')
expect(html).toContain('MISSING:X-TEST-INPUT')
}
expect(html).toContain('</html>')
})
}
}
)
})
if (!isNextDev) {
describe('HTML Fallback', () => {
// We'll attempt to load N pages, all of which will not exist in the cache.
const pathnames: Array<{
pathname: string
slug: string
client: boolean
}> = []
const patterns: Array<
[generator: (slug: string) => string, client: boolean, nested: boolean]
> = [
[(slug) => `/fallback/params/${slug}`, false, false],
[(slug) => `/fallback/use-pathname/${slug}`, true, false],
[(slug) => `/fallback/use-params/${slug}`, true, false],
[
(slug) => `/fallback/use-selected-layout-segment/${slug}`,
true,
false,
],
[
(slug) => `/fallback/use-selected-layout-segments/${slug}`,
true,
false,
],
[(slug) => `/fallback/nested/params/${slug}`, false, true],
[(slug) => `/fallback/nested/use-pathname/${slug}`, true, true],
[(slug) => `/fallback/nested/use-params/${slug}`, true, true],
[
(slug) => `/fallback/nested/use-selected-layout-segment/${slug}`,
true,
true,
],
[
(slug) => `/fallback/nested/use-selected-layout-segments/${slug}`,
true,
true,
],
]
const pad = (num: number) => String(num).padStart(2, '0')
for (let i = 1; i < 2; i++) {
for (const [pattern, client, nested] of patterns) {
let slug: string
if (nested) {
const slugs: string[] = []
for (let j = i + 1; j < i + 2; j++) {
slugs.push(`slug-${pad(i)}/slug-${pad(j)}`)
}
slug = slugs.join('/')
} else {
slug = `slug-${pad(i)}`
}
pathnames.push({ pathname: pattern(slug), slug, client })
}
}
describe.each(pathnames)(
'for $pathname',
({ pathname, slug, client }) => {
it('should render the fallback HTML immediately', async () => {
const [staticPart, dynamicPart] =
await splitResponseWithPPRSentinel(async () => {
const res = await next.fetch(pathname)
expect(res.status).toBe(200)
return res.body
})
// Expect that there is a static part of the response, implying that
// the fallback shell was sent immediately.
expect(staticPart.length).toBeGreaterThan(0)
// Expect that there is a dynamic part of the response, implying that
// the dynamic part was sent after the static part.
expect(dynamicPart.length).toBeGreaterThan(0)
if (client) {
const browser = await next.browser(pathname)
try {
await browser.waitForElementByCss('[data-slug]')
expect(
await browser.elementByCss('[data-slug]').text()
).toContain(slug)
} finally {
await browser.close()
}
} else {
// The static part should not contain the dynamic parameter.
let $ = cheerio.load(staticPart)
let data = $('[data-slug]').text()
expect(data).not.toContain(slug)
expect($('[data-slug]').closest('[hidden]').length).toBe(0)
// The dynamic part should contain the dynamic parameter.
$ = cheerio.load(dynamicPart)
data = $('[data-slug]').text()
expect(data).toContain(slug)
expect($('[data-slug]').closest('[hidden]').length).toBe(1)
// The static part should contain the fallback shell.
expect(staticPart).toContain('data-fallback')
}
})
}
)
describe('Dynamic Shell', () => {
it('should render the fallback shell on first visit', async () => {
const random = Math.random().toString(16).slice(2)
const pathname = `/fallback/dynamic/params/on-first-visit-${random}`
const $ = await next.render$(pathname)
expect($('[data-slug]').closest('[hidden]').length).toBe(1)
expect($('[data-agent]').closest('[hidden]').length).toBe(1)
})
it('should render the fallback shell every time', async () => {
const random = Math.random().toString(16).slice(2)
const pathname = `/fallback/dynamic/params/on-second-visit-${random}`
let $ = await next.render$(pathname)
expect($('[data-slug]').closest('[hidden]').length).toBe(1)
expect($('[data-agent]').closest('[hidden]').length).toBe(1)
for (let i = 0; i < 10; i++) {
$ = await next.render$(pathname)
expect($('[data-slug]').closest('[hidden]').length).toBe(1)
expect($('[data-agent]').closest('[hidden]').length).toBe(1)
}
})
it('should render the fallback shell even if the page is static', async () => {
const random = Math.random().toString(16).slice(2)
const pathname = `/fallback/params/on-second-visit-${random}`
// Expect that the slug had to be resumed.
let $ = await next.render$(pathname)
expect($('[data-slug]').closest('[hidden]').length).toBe(1)
for (let i = 0; i < 10; i++) {
$ = await next.render$(pathname)
expect($('[data-slug]').closest('[hidden]').length).toBe(1)
}
})
it('will not revalidate the fallback shell', async () => {
const random = Math.random().toString(16).slice(2)
const pathname = `/fallback/dynamic/params/revalidate-${random}`
let $ = await next.render$(pathname)
const fallbackID = $('[data-layout]').data('layout') as string
// Now let's revalidate the page.
await next.fetch(
`/api/revalidate?pathname=${encodeURIComponent(pathname)}`
)
// We expect to get the fallback shell again.
$ = await next.render$(pathname)
expect($('[data-layout]').data('layout')).toBe(fallbackID)
// Let's wait for the page to be revalidated.
await retry(async () => {
$ = await next.render$(pathname)
const newDynamicID = $('[data-layout]').data('layout') as string
expect(newDynamicID).toBe(fallbackID)
})
})
/**
* This test is really here to just to force the the suite to have the expected route
* as part of the build. If this failed we'd get a build error and all the tests would fail
*/
it('will allow dynamic fallback shells even when static is enforced', async () => {
const random = Math.random().toString(16).slice(2)
const pathname = `/fallback/dynamic/params/revalidate-${random}`
let $ = await next.render$(pathname)
expect($('[data-slug]').text()).toBe(`revalidate-${random}`)
})
})
it('should allow client layouts without postponing fallback if params are not accessed', async () => {
const $ = await next.render$('/fallback/client/params/page/slug-01')
let selector = $(
'[data-file="app/fallback/client/params/[slug]/loading"]'
)
expect(selector.length).toBe(1)
expect(selector.closest('[hidden]').length).toBe(0)
selector = $('[data-file="app/fallback/client/params/[slug]/layout"]')
expect(selector.length).toBe(1)
expect(selector.closest('[hidden]').length).toBe(0)
selector = $('[data-file="app/fallback/client/params/[slug]/page"]')
expect(selector.length).toBe(1)
expect(selector.closest('[hidden]').length).toBe(1)
})
it('should postpone in client layout when fallback params are accessed', async () => {
const $ = await next.render$('/fallback/client/params/layout/slug-01')
let selector = $('[data-fallback="true"]')
expect(selector.length).toBe(1)
expect(selector.closest('[hidden]').length).toBe(0)
selector = $('[data-file="app/fallback/client/params/[slug]/layout"]')
expect(selector.length).toBe(1)
expect(selector.closest('[hidden]').length).toBe(1)
selector = $('[data-file="app/fallback/client/params/[slug]/page"]')
expect(selector.length).toBe(1)
expect(selector.closest('[hidden]').length).toBe(1)
})
})
}
describe('Navigation Signals', () => {
describe.each([
{
signal: 'notFound()' as const,
statusCode: 404,
pathnames: ['/navigation/not-found', '/navigation/not-found/dynamic'],
},
{
signal: 'redirect()' as const,
statusCode: 307,
pathnames: ['/navigation/redirect', '/navigation/redirect/dynamic'],
},
])('$signal', ({ signal, statusCode, pathnames }) => {
describe.each(pathnames)('for %s', (pathname) => {
it('should have correct headers', async () => {
const res = await next.fetch(pathname, {
redirect: 'manual',
})
expect(res.status).toEqual(signal === 'redirect()' ? 307 : 404)
expect(res.headers.get('content-type')).toEqual(
'text/html; charset=utf-8'
)
if (isNextStart) {
expect(res.headers.get('cache-control')).toEqual(
's-maxage=31536000'
)
}
if (isNextDeploy) {
expectDirectives(res.headers.get('cache-control') || '', [
'public',
'max-age=0',
'must-revalidate',
])
}
if (signal === 'redirect()') {
const location = res.headers.get('location')
expect(location).not.toBeNull()
expect(typeof location).toEqual('string')
// The URL returned in `Location` is absolute, so we need to parse it
// to get the pathname.
const url = new URL(location)
expect(url.pathname).toEqual('/navigation/redirect/location')
}
})
if (pathname.endsWith('/dynamic') && !isNextDeploy) {
it('should cache the static part', async () => {
const [staticPart, dynamicPart] =
await splitResponseWithPPRSentinel(async () => {
const res = await next.fetch(pathname, {
redirect: 'manual',
})
expect(res.status).toBe(statusCode)
return res.body
})
expect(staticPart.length).toBeGreaterThan(0)
expect(dynamicPart.length).toEqual(0)
})
}
})
})
})
if (!isNextDev) {
describe('Prefetch RSC Response', () => {
describe.each(pages)('for $pathname', ({ pathname, revalidate }) => {
it('should have correct headers', async () => {
await retry(async () => {
const headers = {
rsc: '1',
'next-router-prefetch': '1',
}
const urlWithCacheBusting = await addCacheBustingSearchParam(
pathname,
headers
)
const res = await next.fetch(urlWithCacheBusting, {
headers,
})
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual('text/x-component')
if (isNextDeploy) {
expectDirectives(res.headers.get('cache-control') || '', [
'public',
'max-age=0',
'must-revalidate',
])
} else {
expect(res.headers.get('cache-control')).toEqual(
revalidate === undefined
? `s-maxage=31536000`
: `s-maxage=${revalidate}, stale-while-revalidate=${31536000 - revalidate}`
)
}
expect(getCacheHeader(res)).toBe('HIT')
})
})
it('should not contain dynamic content', async () => {
const unexpected = `${Date.now()}:${Math.random()}`
const headers = {
rsc: '1',
'next-router-prefetch': '1',
'X-Test-Input': unexpected,
}
const urlWithCacheBusting = await addCacheBustingSearchParam(
pathname,
headers
)
const res = await next.fetch(urlWithCacheBusting, {
headers,
})
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual('text/x-component')
const text = await res.text()
expect(text).not.toContain(unexpected)
})
})
})
describe('Dynamic RSC Response', () => {
describe.each(pages)('for $pathname', ({ pathname, dynamic }) => {
it('should have correct headers', async () => {
const headers = { rsc: '1' }
const urlWithCacheBusting = await addCacheBustingSearchParam(
pathname,
headers
)
let res = await next.fetch(urlWithCacheBusting, {
headers,
})
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual('text/x-component')
expectDirectives(res.headers.get('cache-control') || '', [
'private',
'no-store',
'no-cache',
'max-age=0',
'must-revalidate',
])
if (isNextDeploy) {
expect(getCacheHeader(res)).toMatch(/MISS|HIT|PRERENDER/)
} else {
expect(res.headers.get('x-nextjs-cache')).toEqual(null)
}
})
if (dynamic === true || dynamic === 'force-dynamic') {
it('should contain dynamic content', async () => {
const expected = `${Date.now()}:${Math.random()}`
const headers = {
rsc: '1',
'X-Test-Input': expected,
}
const urlWithCacheBusting = await addCacheBustingSearchParam(
pathname,
headers
)
const res = await next.fetch(urlWithCacheBusting, {
headers,
})
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual('text/x-component')
const text = await res.text()
expect(text).toContain(expected)
})
} else {
it('should not contain dynamic content', async () => {
const unexpected = `${Date.now()}:${Math.random()}`
const headers = {
rsc: '1',
'X-Test-Input': unexpected,
}
const urlWithCacheBusting = await addCacheBustingSearchParam(
pathname,
headers
)
const res = await next.fetch(urlWithCacheBusting, {
headers,
})
expect(res.status).toEqual(200)
expect(res.headers.get('content-type')).toEqual('text/x-component')
const text = await res.text()
expect(text).not.toContain(unexpected)
})
}
})
})
describe('Dynamic Data pages', () => {
describe('Optimistic UI', () => {
it('should initially render with optimistic UI', async () => {
const $ = await next.render$('/dynamic-data?foo=bar')
// We defined some server html let's make sure it flushed both in the head
// There may be additional flushes in the body but we want to ensure that
// server html is getting inserted in the shell correctly here
const serverHTML = $('head meta[name="server-html"]')
expect(serverHTML.length).toEqual(1)
expect($(serverHTML[0]).attr('content')).toEqual('0')
// We expect the server HTML to be the optimistic output
expect($('#foosearch').text()).toEqual('foo search: optimistic')
// We expect hydration to patch up the render with dynamic data
// from the resume
const browser = await next.browser('/dynamic-data?foo=bar')
try {
await browser.waitForElementByCss('#foosearch')
expect(
await browser.eval(
'document.getElementById("foosearch").textContent'
)
).toEqual('foo search: bar')
} finally {
await browser.close()
}
})
it('should render entirely statically with force-static', async () => {
const $ = await next.render$('/dynamic-data/force-static?foo=bar')
// We defined some server html let's make sure it flushed both in the head
// There may be additional flushes in the body but we want to ensure that
// server html is getting inserted in the shell correctly here
const serverHTML = $('head meta[name="server-html"]')
expect(serverHTML.length).toEqual(1)
expect($(serverHTML[0]).attr('content')).toEqual('0')
// We expect the server HTML to be forced static so no params
// were made available but also nothing threw and was caught for
// optimistic UI
expect($('#foosearch').text()).toEqual('foo search: ')
// There is no hydration mismatch, we continue to have empty searchParams
const browser = await next.browser(
'/dynamic-data/force-static?foo=bar'
)
try {
await browser.waitForElementByCss('#foosearch')
expect(
await browser.eval(
'document.getElementById("foosearch").textContent'
)
).toEqual('foo search: ')
} finally {
await browser.close()
}
})
it('should render entirely dynamically when force-dynamic', async () => {
const $ = await next.render$('/dynamic-data/force-dynamic?foo=bar')
// We defined some server html let's make sure it flushed both in the head
// There may be additional flushes in the body but we want to ensure that
// server html is getting inserted in the shell correctly here
const serverHTML = $('head meta[name="server-html"]')
expect(serverHTML.length).toEqual(1)
expect($(serverHTML[0]).attr('content')).toEqual('0')
// We expect the server HTML to render dynamically
expect($('#foosearch').text()).toEqual('foo search: bar')
})
})
describe('Incidental postpones', () => {
it('should initially render with optimistic UI', async () => {
const $ = await next.render$(
'/dynamic-data/incidental-postpone?foo=bar'
)
// We defined some server html let's make sure it flushed both in the head
// There may be additional flushes in the body but we want to ensure that
// server html is getting inserted in the shell correctly here
const serverHTML = $('head meta[name="server-html"]')
expect(serverHTML.length).toEqual(1)
expect($(serverHTML[0]).attr('content')).toEqual('0')
// We expect the server HTML to be the optimistic output
expect($('#foosearch').text()).toEqual('foo search: optimistic')
// We expect hydration to patch up the render with dynamic data
// from the resume
const browser = await next.browser(
'/dynamic-data/incidental-postpone?foo=bar'
)
try {
await browser.waitForElementByCss('#foosearch')
expect(
await browser.eval(
'document.getElementById("foosearch").textContent'
)
).toEqual('foo search: bar')
} finally {
await browser.close()
}
})
it('should render entirely statically with force-static', async () => {
const $ = await next.render$(
'/dynamic-data/incidental-postpone/force-static?foo=bar'
)
// We defined some server html let's make sure it flushed both in the head
// There may be additional flushes in the body but we want to ensure that
// server html is getting inserted in the shell correctly here
const serverHTML = $('head meta[name="server-html"]')
expect(serverHTML.length).toEqual(1)
expect($(serverHTML[0]).attr('content')).toEqual('0')
// We expect the server HTML to be forced static so no params
// were made available but also nothing threw and was caught for
// optimistic UI
expect($('#foosearch').text()).toEqual('foo search: ')
// There is no hydration mismatch, we continue to have empty searchParams
const browser = await next.browser(
'/dynamic-data/incidental-postpone/force-static?foo=bar'
)
try {
await browser.waitForElementByCss('#foosearch')
expect(
await browser.eval(
'document.getElementById("foosearch").textContent'
)
).toEqual('foo search: ')
} finally {
await browser.close()
}
})
it('should render entirely dynamically when force-dynamic', async () => {
const $ = await next.render$(
'/dynamic-data/incidental-postpone/force-dynamic?foo=bar'
)
// We defined some server html let's make sure it flushed both in the head
// There may be additional flushes in the body but we want to ensure that
// server html is getting inserted in the shell correctly here
const serverHTML = $('head meta[name="server-html"]')
expect(serverHTML.length).toEqual(1)
expect($(serverHTML[0]).attr('content')).toEqual('0')
// We expect the server HTML to render dynamically
expect($('#foosearch').text()).toEqual('foo search: bar')
})
})
})
}
})