rsnext/test/e2e/multi-zone/multi-zone.test.ts
Agustín Tornielli 447b416f4d
Fix: HMR in multi-zone handling 🌱 (#59471)
### What?
When running a
[multi-zone](https://github.com/vercel/next.js/tree/canary/examples/with-zones)
app in dev, guest app pages would infinitely reload if you change the
basePath of the host app to the default one (omit basePath settings in
next.config.js) (empty string `""` as per Next.js docs).

### Why?
The HMR upgrade request would fail and get caught into a retry loop. In
the multi-zone case, they fail because the upgrade request would be sent
again for a request that had already been upgraded. This resulted in a
"server.handleUpgrade() was called more than once with the same socket"
error, causing the upgrade request to fail.

Every time a retry occurred, the page would trigger a full refresh since
certain HMR errors cause the browser to reload.

### How?
This ensures the upgrade handler only responds to requests that match
the configured basePath (considering when there is no basePath). Default
basePath for Next.js applications it's an empty string `""`.
 
Ref: https://nextjs.org/docs/app/api-reference/next-config-js/basePath

Other fixes & updates related to the bug:
- Updated test apps to avoid having issues regarding client & server
mismatch for dates
- Added default use case in e2e tests, where you have a default Next.js
application where the basePath it's the default one and a guest app that
it's being routed by the main one through Next.js rewrites.

Closes NEXT-1797
Fixes #59161
Fixes #56615
Fixes #54454

---------

Co-authored-by: Zack Tanner <zacktanner@gmail.com>
2024-01-08 23:55:41 +00:00

91 lines
2.6 KiB
TypeScript

import { createNextDescribe } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'
import path from 'path'
createNextDescribe(
'multi-zone',
{
files: path.join(__dirname, 'app'),
skipDeployment: true,
buildCommand: 'pnpm build',
startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start',
packageJson: {
scripts: {
'post-build': 'echo done',
},
},
},
({ next, isNextDev }) => {
it.each([
{ pathname: '/', content: ['hello from host app'] },
{ pathname: '/guest', content: ['hello from guest app'] },
{
pathname: '/blog/post-1',
content: ['hello from host app /blog/[slug]'],
},
{
pathname: '/guest/blog/post-1',
content: ['hello from guest app /blog/[slug]'],
},
{
pathname: '/guest/another/post-1',
content: ['hello from guest app /another/[slug]'],
},
])(
'should correctly respond for $pathname',
async ({ pathname, content }) => {
const res = await next.fetch(pathname, {
redirect: 'manual',
})
expect(res.status).toBe(200)
const html = await res.text()
for (const item of content) {
expect(html).toContain(item)
}
}
)
if (isNextDev) {
async function runHMRTest(app: string) {
const isHostApp = app === 'host'
const browser = await next.browser(isHostApp ? '/' : app)
expect(await browser.elementByCss('body').text()).toContain(
`hello from ${app} app`
)
const initialTimestamp = await browser.elementById('now').text()
expect(await browser.elementByCss('body').text()).not.toContain(
'hmr content'
)
await waitFor(1000)
// verify that the page isn't unexpectedly reloading in the background
const newTimestamp = await browser.elementById('now').text()
expect(newTimestamp).toBe(initialTimestamp)
// trigger HMR
const filePath = `apps/${app}/pages/index.tsx`
const content = await next.readFile(filePath)
const patchedContent = content.replace(
`const editedContent = ''`,
`const editedContent = 'hmr content'`
)
await next.patchFile(filePath, patchedContent)
await check(() => browser.elementByCss('body').text(), /hmr content/)
// restore original content
await next.patchFile(filePath, content)
}
it('should support HMR in both apps', async () => {
await runHMRTest('host')
await runHMRTest('guest')
})
}
}
)