mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What? Migrate remaining direct `next-webdriver` test callers that have a `NextInstance` to `next.browser()`, and expose the shared `Playwright` browser type from `e2e-utils`. ### Why? `NextInstance.browser` should be the supported browser-opening interface for test fixtures, with `next-webdriver` kept as the private implementation detail. ### How? Updated affected development, e2e, and production tests to call `next.browser()` directly, passing `baseUrl` where tests intentionally target a manually spawned or proxied server. Shared helpers now receive browser callbacks from the test context, and browser types import `Playwright` from `e2e-utils` instead of deriving from `next.browser` or importing from private paths. <!-- NEXT_JS_LLM_PR -->
407 lines
12 KiB
TypeScript
407 lines
12 KiB
TypeScript
import { FileRef, nextTestSetup } from 'e2e-utils'
|
|
import { check, fetchViaHTTP } from 'next-test-utils'
|
|
import {
|
|
NEXT_HMR_REFRESH_HEADER,
|
|
NEXT_ROUTER_PREFETCH_HEADER,
|
|
NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
|
|
NEXT_ROUTER_STATE_TREE_HEADER,
|
|
NEXT_RSC_UNION_QUERY,
|
|
RSC_HEADER,
|
|
} from 'next/dist/client/components/app-router-headers'
|
|
import { join } from 'path'
|
|
import cheerio from 'cheerio'
|
|
|
|
describe('skip-trailing-slash-redirect', () => {
|
|
const { next } = nextTestSetup({
|
|
files: new FileRef(join(__dirname, 'app')),
|
|
dependencies: {},
|
|
})
|
|
|
|
// the tests below are run in both pages and app dir to ensure the behavior is the same
|
|
// the other cases aren't added to this block since they are either testing pages-specific behavior
|
|
// or aren't specific to either router implementation
|
|
async function runSharedTests(basePath: string) {
|
|
it('should not apply trailing slash redirect (with slash)', async () => {
|
|
const res = await fetchViaHTTP(
|
|
next.url,
|
|
`${basePath}another/`,
|
|
undefined,
|
|
{
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('another page')
|
|
})
|
|
|
|
it('should not apply trailing slash redirect (without slash)', async () => {
|
|
const res = await fetchViaHTTP(
|
|
next.url,
|
|
`${basePath}another`,
|
|
undefined,
|
|
{
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('another page')
|
|
})
|
|
|
|
it('should preserve original trailing slashes to links on client', async () => {
|
|
const browser = await next.browser(basePath)
|
|
await browser.eval('window.beforeNav = 1')
|
|
|
|
expect(
|
|
new URL(
|
|
await browser.elementByCss('#to-another').getAttribute('href'),
|
|
'http://n'
|
|
).pathname
|
|
).toBe(`${basePath}another`)
|
|
|
|
expect(
|
|
new URL(
|
|
await browser
|
|
.elementByCss('#to-another-with-slash')
|
|
.getAttribute('href'),
|
|
'http://n'
|
|
).pathname
|
|
).toBe(`${basePath}another/`)
|
|
|
|
await browser.elementByCss('#to-another').click()
|
|
await browser.waitForElementByCss('#another')
|
|
|
|
expect(await browser.eval('window.location.pathname')).toBe(
|
|
`${basePath}another`
|
|
)
|
|
|
|
await browser.back().waitForElementByCss('#to-another')
|
|
|
|
expect(
|
|
new URL(
|
|
await browser
|
|
.elementByCss('#to-another-with-slash')
|
|
.getAttribute('href'),
|
|
'http://n'
|
|
).pathname
|
|
).toBe(`${basePath}another/`)
|
|
|
|
await browser.elementByCss('#to-another-with-slash').click()
|
|
await browser.waitForElementByCss('#another')
|
|
|
|
expect(await browser.eval('window.location.pathname')).toBe(
|
|
`${basePath}another/`
|
|
)
|
|
|
|
await browser.back().waitForElementByCss('#to-another')
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
})
|
|
|
|
it('should respond to index correctly', async () => {
|
|
const res = await fetchViaHTTP(next.url, basePath, undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('index page')
|
|
})
|
|
|
|
it('should respond to dynamic route correctly', async () => {
|
|
const res = await fetchViaHTTP(
|
|
next.url,
|
|
`${basePath}blog/first`,
|
|
undefined,
|
|
{
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('blog page')
|
|
})
|
|
|
|
it('should navigate client side correctly', async () => {
|
|
const browser = await next.browser(basePath)
|
|
|
|
expect(await browser.eval('location.pathname')).toBe(basePath)
|
|
|
|
await browser.elementByCss('#to-another').click()
|
|
await browser.waitForElementByCss('#another')
|
|
|
|
expect(await browser.eval('location.pathname')).toBe(`${basePath}another`)
|
|
await browser.back()
|
|
await browser.waitForElementByCss('#index')
|
|
|
|
expect(await browser.eval('location.pathname')).toBe(basePath)
|
|
|
|
await browser.elementByCss('#to-blog-first').click()
|
|
await browser.waitForElementByCss('#blog')
|
|
|
|
expect(await browser.eval('location.pathname')).toBe(
|
|
`${basePath}blog/first`
|
|
)
|
|
})
|
|
}
|
|
|
|
it('should parse locale info for data request correctly', async () => {
|
|
const pathname = `/_next/data/${next.buildId}/ja-jp/locale-test.json`
|
|
const res = await next.fetch(pathname)
|
|
|
|
expect(await res.json()).toEqual({
|
|
locale: 'ja-jp',
|
|
pathname,
|
|
})
|
|
})
|
|
|
|
it.each(['EN', 'JA-JP'])(
|
|
'should be able to redirect locale casing $1',
|
|
async (locale) => {
|
|
const res = await next.fetch(`/${locale}`, { redirect: 'manual' })
|
|
expect(res.status).toBe(307)
|
|
expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
|
|
`/${locale.toLowerCase()}`
|
|
)
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
{ pathname: '/chained-rewrite-ssg' },
|
|
{ pathname: '/chained-rewrite-static' },
|
|
{ pathname: '/chained-rewrite-ssr' },
|
|
{ pathname: '/docs/first' },
|
|
{ pathname: '/docs-auto-static/first' },
|
|
{ pathname: '/docs-ssr/first' },
|
|
])(
|
|
'should handle external rewrite correctly $pathname',
|
|
async ({ pathname }) => {
|
|
const res = await fetchViaHTTP(next.url, pathname, undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
|
|
if (!pathname.includes('static')) {
|
|
expect(JSON.parse($('#props').text()).params).toEqual({
|
|
slug: 'first',
|
|
})
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
slug: 'first',
|
|
})
|
|
}
|
|
|
|
const browser = await next.browser('/docs', {
|
|
waitHydration: false,
|
|
})
|
|
await check(
|
|
() => browser.eval('next.router.isReady ? "yes": "no"'),
|
|
'yes'
|
|
)
|
|
await check(() => browser.elementByCss('#mounted').text(), 'yes')
|
|
await browser.eval('window.beforeNav = 1')
|
|
|
|
await browser.elementByCss(`#${pathname.replace(/\//g, '')}`).click()
|
|
await check(async () => {
|
|
const query = JSON.parse(await browser.elementByCss('#query').text())
|
|
expect(query).toEqual({ slug: 'first' })
|
|
return 'success'
|
|
}, 'success')
|
|
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
}
|
|
)
|
|
|
|
it('should allow rewriting invalid buildId correctly', async () => {
|
|
const res = await fetchViaHTTP(
|
|
next.url,
|
|
'/_next/data/missing-id/hello.json',
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-nextjs-data': '1',
|
|
},
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('Example Domain')
|
|
|
|
if (!(global as any).isNextDeploy) {
|
|
await check(() => next.cliOutput, /missing-id rewrite/)
|
|
expect(next.cliOutput).toContain('/_next/data/missing-id/hello.json')
|
|
}
|
|
})
|
|
|
|
it('should provide original _next/data URL with skipProxyUrlNormalize', async () => {
|
|
const res = await fetchViaHTTP(
|
|
next.url,
|
|
`/_next/data/${next.buildId}/valid.json`,
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-nextjs-data': '1',
|
|
},
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('Example Domain')
|
|
})
|
|
|
|
it('should receive _rsc query and all RSC headers in middleware request', async () => {
|
|
const rscQuery = 'cache-buster'
|
|
const rscHeaders = {
|
|
[RSC_HEADER]: '1',
|
|
[NEXT_ROUTER_STATE_TREE_HEADER]: 'test-tree',
|
|
[NEXT_ROUTER_PREFETCH_HEADER]: '1',
|
|
[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: '/_tree',
|
|
[NEXT_HMR_REFRESH_HEADER]: '1',
|
|
'x-nextjs-data': '1',
|
|
}
|
|
const res = await fetchViaHTTP(
|
|
next.url,
|
|
`/rsc-valid?${NEXT_RSC_UNION_QUERY}=${rscQuery}`,
|
|
undefined,
|
|
{ headers: rscHeaders }
|
|
)
|
|
expect(res.status).toBe(200)
|
|
|
|
expect(await res.json()).toEqual({
|
|
pathname: `/rsc-valid`,
|
|
rscQuery,
|
|
rscHeaders: {
|
|
rsc: rscHeaders[RSC_HEADER],
|
|
nextRouterStateTree: rscHeaders[NEXT_ROUTER_STATE_TREE_HEADER],
|
|
nextRouterPrefetch: rscHeaders[NEXT_ROUTER_PREFETCH_HEADER],
|
|
nextRouterSegmentPrefetch:
|
|
rscHeaders[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER],
|
|
nextHmrRefresh: rscHeaders[NEXT_HMR_REFRESH_HEADER],
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should allow response body from middleware with flag', async () => {
|
|
const res = await fetchViaHTTP(next.url, '/middleware-response-body')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('x-from-middleware')).toBe('true')
|
|
expect(await res.text()).toBe('hello from middleware')
|
|
})
|
|
|
|
it('should merge cookies from middleware and API routes correctly', async () => {
|
|
const res = await fetchViaHTTP(next.url, '/api/test-cookie', undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('set-cookie')).toEqual(
|
|
'from-middleware=1; Path=/, hello=From API'
|
|
)
|
|
})
|
|
|
|
it('should merge cookies from middleware and edge API routes correctly', async () => {
|
|
const res = await fetchViaHTTP(
|
|
next.url,
|
|
'/api/test-cookie-edge',
|
|
undefined,
|
|
{
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('set-cookie')).toEqual(
|
|
'from-middleware=1; Path=/, hello=From%20API; Path=/'
|
|
)
|
|
})
|
|
|
|
if ((global as any).isNextStart) {
|
|
it('should not have trailing slash redirects in manifest', async () => {
|
|
const routesManifest = JSON.parse(
|
|
await next.readFile('.next/routes-manifest.json')
|
|
)
|
|
|
|
expect(
|
|
routesManifest.redirects.some((redirect) => {
|
|
return (
|
|
redirect.statusCode === 308 &&
|
|
(redirect.destination === '/:path+' ||
|
|
redirect.destination === '/:path+/')
|
|
)
|
|
})
|
|
).toBe(false)
|
|
})
|
|
}
|
|
|
|
it('should correct skip URL normalizing in middleware', async () => {
|
|
let res = await fetchViaHTTP(
|
|
next.url,
|
|
`/_next/data/${next.buildId}/middleware-rewrite-with-slash.json`,
|
|
undefined,
|
|
{ redirect: 'manual', headers: { 'x-nextjs-data': '1' } }
|
|
)
|
|
expect(res.headers.get('x-nextjs-rewrite').endsWith('/another/')).toBe(true)
|
|
|
|
res = await fetchViaHTTP(
|
|
next.url,
|
|
`/_next/data/${next.buildId}/middleware-rewrite-without-slash.json`,
|
|
undefined,
|
|
{ redirect: 'manual', headers: { 'x-nextjs-data': '1' } }
|
|
)
|
|
expect(res.headers.get('x-nextjs-rewrite').endsWith('/another')).toBe(true)
|
|
|
|
res = await fetchViaHTTP(
|
|
next.url,
|
|
'/middleware-redirect-external-with',
|
|
undefined,
|
|
{ redirect: 'manual' }
|
|
)
|
|
expect(res.status).toBe(307)
|
|
expect(res.headers.get('Location')).toBe(
|
|
'https://example.vercel.sh/somewhere/'
|
|
)
|
|
|
|
res = await fetchViaHTTP(
|
|
next.url,
|
|
'/middleware-redirect-external-without',
|
|
undefined,
|
|
{ redirect: 'manual' }
|
|
)
|
|
expect(res.status).toBe(307)
|
|
expect(res.headers.get('Location')).toBe(
|
|
'https://example.vercel.sh/somewhere'
|
|
)
|
|
})
|
|
|
|
it('should apply config redirect correctly', async () => {
|
|
const res = await fetchViaHTTP(next.url, '/redirect-me', undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(307)
|
|
expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
|
|
'/another'
|
|
)
|
|
})
|
|
|
|
it('should apply config rewrites correctly', async () => {
|
|
const res = await fetchViaHTTP(next.url, '/rewrite-me', undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('another page')
|
|
})
|
|
|
|
it('should not apply trailing slash on load on client', async () => {
|
|
let browser = await next.browser('/another')
|
|
await check(() => browser.eval('next.router.isReady ? "yes": "no"'), 'yes')
|
|
|
|
expect(await browser.eval('location.pathname')).toBe('/another')
|
|
|
|
browser = await next.browser('/another/')
|
|
await check(() => browser.eval('next.router.isReady ? "yes": "no"'), 'yes')
|
|
|
|
expect(await browser.eval('location.pathname')).toBe('/another/')
|
|
})
|
|
|
|
describe('pages dir', () => {
|
|
runSharedTests('/')
|
|
})
|
|
|
|
describe('app dir - skip trailing slash redirect', () => {
|
|
runSharedTests('/with-app-dir/')
|
|
})
|
|
})
|