diff --git a/test/e2e/app-dir/empty-fallback-shells/app/with-cached-io/without-suspense/layout.jsx b/test/e2e/app-dir/empty-fallback-shells/app/with-cached-io/without-suspense/layout.jsx
index a9acdbda7e2b..ad3ada30c1da 100644
--- a/test/e2e/app-dir/empty-fallback-shells/app/with-cached-io/without-suspense/layout.jsx
+++ b/test/e2e/app-dir/empty-fallback-shells/app/with-cached-io/without-suspense/layout.jsx
@@ -6,8 +6,8 @@ export default async function Layout({ children }) {
return (
-
- Layout: {new Date().toISOString()}
+
+ Layout: {new Date().toISOString()} ({getSentinelValue()})
{children}
diff --git a/test/e2e/app-dir/empty-fallback-shells/app/without-io/[slug]/page.jsx b/test/e2e/app-dir/empty-fallback-shells/app/without-io/[slug]/page.jsx
index aa74c6b2a192..1d7464925457 100644
--- a/test/e2e/app-dir/empty-fallback-shells/app/without-io/[slug]/page.jsx
+++ b/test/e2e/app-dir/empty-fallback-shells/app/without-io/[slug]/page.jsx
@@ -1,6 +1,6 @@
export default async function Page({ params }) {
const { slug } = await params
- return
Hello /{slug}
+ return
Hello /{slug}
}
export async function generateStaticParams() {
diff --git a/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts b/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts
index cfc4ce7ece8b..b2082f93ba4a 100644
--- a/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts
+++ b/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts
@@ -7,17 +7,19 @@ describe('empty-fallback-shells', () => {
describe('without IO', () => {
it('should start and not postpone the response', async () => {
- const res = await next.fetch('/without-io/world')
- const html = await res.text()
- expect(html).toContain('hello-world')
+ const { browser, response } =
+ await next.browserWithResponse('/without-io/world')
+
+ expect(await browser.elementById('slug').text()).toBe('Hello /world')
+ const headers = response.headers()
if (isNextDeploy) {
- expect(res.headers.get('x-matched-path')).toBe('/without-io/[slug]')
+ expect(headers['x-matched-path']).toBe('/without-io/[slug]')
}
// If we didn't use the fallback shell, then we didn't postpone the
// response and therefore shouldn't have sent the postponed header.
- expect(res.headers.get('x-nextjs-postponed')).not.toBe('1')
+ expect(headers['x-nextjs-postponed']).not.toBe('1')
})
})
@@ -25,75 +27,75 @@ describe('empty-fallback-shells', () => {
describe('and the page wrapped in Suspense', () => {
describe('and the params accessed in the cached page', () => {
it('resumes a postponed fallback shell', async () => {
- const res = await next.fetch(
+ const { browser, response } = await next.browserWithResponse(
'/with-cached-io/with-suspense/params-in-page/bar'
)
- const html = await res.text()
- expect(html).toContain('page-bar')
+ const lastModified = await browser.elementById('last-modified').text()
+ expect(lastModified).toInclude('Page /bar')
+ expect(lastModified).toInclude('runtime')
- if (isNextDev) {
- expect(html).toContain('layout-runtime')
- } else {
- expect(html).toContain('layout-buildtime')
- }
+ const layout = await browser.elementById('layout').text()
+ expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')
+
+ const headers = response.headers()
if (isNextDeploy) {
- expect(res.headers.get('x-matched-path')).toBe(
+ expect(headers['x-matched-path']).toBe(
'/with-cached-io/with-suspense/params-in-page/[slug]'
)
} else if (isNextStart) {
- expect(res.headers.get('x-nextjs-postponed')).toBe('1')
+ expect(headers['x-nextjs-postponed']).toBe('1')
}
})
})
describe('and the params accessed in cached non-page function', () => {
it('resumes a postponed fallback shell', async () => {
- const res = await next.fetch(
+ const { browser, response } = await next.browserWithResponse(
'/with-cached-io/with-suspense/params-not-in-page/bar'
)
- const html = await res.text()
- expect(html).toContain('page-bar')
+ const lastModified = await browser.elementById('last-modified').text()
+ expect(lastModified).toInclude('Page /bar')
+ expect(lastModified).toInclude('runtime')
- if (isNextDev) {
- expect(html).toContain('layout-runtime')
- } else {
- expect(html).toContain('layout-buildtime')
- }
+ const layout = await browser.elementById('layout').text()
+ expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')
+
+ const headers = response.headers()
if (isNextDeploy) {
- expect(res.headers.get('x-matched-path')).toBe(
+ expect(headers['x-matched-path']).toBe(
'/with-cached-io/with-suspense/params-not-in-page/[slug]'
)
} else if (isNextStart) {
- expect(res.headers.get('x-nextjs-postponed')).toBe('1')
+ expect(headers['x-nextjs-postponed']).toBe('1')
}
})
})
describe('and params.then/catch/finally passed to a cached function', () => {
it('resumes a postponed fallback shell', async () => {
- const res = await next.fetch(
+ const { browser, response } = await next.browserWithResponse(
'/with-cached-io/with-suspense/params-then-in-page/bar'
)
- const html = await res.text()
- expect(html).toIncludeRepeated('data-testid="page-bar"', 4)
+ const lastModified = await browser.elementById('last-modified').text()
+ expect(lastModified).toInclude('Page /bar')
+ expect(lastModified).toInclude('runtime')
- if (isNextDev) {
- expect(html).toContain('layout-runtime')
- } else {
- expect(html).toContain('layout-buildtime')
- }
+ const layout = await browser.elementById('layout').text()
+ expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')
+
+ const headers = response.headers()
if (isNextDeploy) {
- expect(res.headers.get('x-matched-path')).toBe(
+ expect(headers['x-matched-path']).toBe(
'/with-cached-io/with-suspense/params-then-in-page/[slug]'
)
} else if (isNextStart) {
- expect(res.headers.get('x-nextjs-postponed')).toBe('1')
+ expect(headers['x-nextjs-postponed']).toBe('1')
}
})
})
@@ -102,77 +104,92 @@ describe('empty-fallback-shells', () => {
describe('and the page not wrapped in Suspense', () => {
describe('and the params accessed in the cached page', () => {
it('does not resume a postponed fallback shell', async () => {
- const res = await next.fetch(
+ const { browser, response } = await next.browserWithResponse(
'/with-cached-io/without-suspense/params-in-page/bar'
)
- const html = await res.text()
- expect(html).toContain('page-bar')
- expect(html).toContain('layout-runtime')
+ const lastModified = await browser.elementById('last-modified').text()
+ expect(lastModified).toInclude('Page /bar')
+ expect(lastModified).toInclude('runtime')
+
+ const layout = await browser.elementById('layout').text()
+ expect(layout).toInclude('runtime')
+
+ const headers = response.headers()
if (isNextDeploy) {
- expect(res.headers.get('x-matched-path')).toBe(
+ expect(headers['x-matched-path']).toBe(
'/with-cached-io/without-suspense/params-in-page/[slug]'
)
- } else {
- expect(res.headers.get('x-nextjs-postponed')).not.toBe('1')
+ } else if (isNextStart) {
+ expect(headers['x-nextjs-postponed']).not.toBe('1')
}
})
it('does not render a fallback shell when using a params placeholder', async () => {
// This should trigger a blocking prerender of the route shell.
- const res = await next.fetch(
+ const { browser, response } = await next.browserWithResponse(
'/with-cached-io/without-suspense/params-in-page/[slug]'
)
- expect(res.status).toBe(200)
-
- const html = await res.text()
+ expect(response.status()).toBe(200)
// This should render the encoded param in the route shell, and not
// interpret the param as a fallback param, and subsequently try to
// render the fallback shell instead, which would fail because of the
// missing parent suspense boundary.
- expect(html).toContain('page-%5Bslug%5D')
+ const lastModified = await browser.elementById('last-modified').text()
+ expect(lastModified).toInclude('Page /%5Bslug%5D')
+ expect(lastModified).toInclude('runtime')
})
})
describe('and the params accessed in a cached non-page function', () => {
it('does not resume a postponed fallback shell', async () => {
- const res = await next.fetch(
+ const { browser, response } = await next.browserWithResponse(
'/with-cached-io/without-suspense/params-not-in-page/bar'
)
- const html = await res.text()
- expect(html).toContain('page-bar')
- expect(html).toContain('layout-runtime')
+ const lastModified = await browser.elementById('last-modified').text()
+ expect(lastModified).toInclude('Page /bar')
+ expect(lastModified).toInclude('runtime')
+
+ const layout = await browser.elementById('layout').text()
+ expect(layout).toInclude('runtime')
+
+ const headers = response.headers()
if (isNextDeploy) {
- expect(res.headers.get('x-matched-path')).toBe(
+ expect(headers['x-matched-path']).toBe(
'/with-cached-io/without-suspense/params-not-in-page/[slug]'
)
- } else {
- expect(res.headers.get('x-nextjs-postponed')).not.toBe('1')
+ } else if (isNextStart) {
+ expect(headers['x-nextjs-postponed']).not.toBe('1')
}
})
})
describe('and params.then/catch/finally passed to a cached function', () => {
it('does not resume a postponed fallback shell', async () => {
- const res = await next.fetch(
+ const { browser, response } = await next.browserWithResponse(
'/with-cached-io/without-suspense/params-then-in-page/bar'
)
- const html = await res.text()
- expect(html).toIncludeRepeated('data-testid="page-bar"', 4)
- expect(html).toContain('layout-runtime')
+ const lastModified = await browser.elementById('last-modified').text()
+ expect(lastModified).toInclude('Page /bar')
+ expect(lastModified).toInclude('runtime')
+
+ const layout = await browser.elementById('layout').text()
+ expect(layout).toInclude('runtime')
+
+ const headers = response.headers()
if (isNextDeploy) {
- expect(res.headers.get('x-matched-path')).toBe(
+ expect(headers['x-matched-path']).toBe(
'/with-cached-io/without-suspense/params-then-in-page/[slug]'
)
- } else {
- expect(res.headers.get('x-nextjs-postponed')).not.toBe('1')
+ } else if (isNextStart) {
+ expect(headers['x-nextjs-postponed']).not.toBe('1')
}
})
})
diff --git a/test/lib/next-modes/base.ts b/test/lib/next-modes/base.ts
index c849aafd5f4a..19aaafc47039 100644
--- a/test/lib/next-modes/base.ts
+++ b/test/lib/next-modes/base.ts
@@ -13,6 +13,7 @@ import cheerio from 'cheerio'
import { once } from 'events'
import { Playwright } from 'next-webdriver'
import escapeStringRegexp from 'escape-string-regexp'
+import { Page, Response } from 'playwright'
type Event = 'stdout' | 'stderr' | 'error' | 'destroy'
export type InstallCommand =
@@ -664,7 +665,7 @@ export class NextInstance {
}
/**
- * Create new browser window for the Next.js app.
+ * Create a new browser window for the Next.js app.
*/
public async browser(
...args: Parameters
>
@@ -672,6 +673,49 @@ export class NextInstance {
return webdriver(this.url, ...args)
}
+ /**
+ * Create a new browser window for the Next.js app, and also return the page's
+ * response.
+ */
+ public async browserWithResponse(
+ ...args: Parameters>
+ ): Promise<{ browser: Playwright; response: Response }> {
+ const [url, options = {}] = args
+
+ let resolveResponse: (response: Response) => void
+
+ const responsePromise = new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ reject(`Timed out waiting for the response of ${url}`)
+ }, 10_000)
+
+ resolveResponse = (response: Response) => {
+ clearTimeout(timer)
+ resolve(response)
+ }
+ })
+
+ const absoluteUrl = new URL(url, this.url).href
+
+ const [browser, response] = await Promise.all([
+ webdriver(this.url, url, {
+ ...options,
+ beforePageLoad(page: Page) {
+ options.beforePageLoad?.(page)
+
+ page.on('response', async (response) => {
+ if (response.url() === absoluteUrl) {
+ resolveResponse(response)
+ }
+ })
+ },
+ }),
+ responsePromise,
+ ])
+
+ return { browser, response }
+ }
+
/**
* Fetch the HTML for the provided page. This is a shortcut for `renderViaHTTP().then(html => cheerio.load(html))`.
*/