Ensure trailing slash is handled correctly with middleware (#38282)
* Ensure trailing slash is handled correctly with middleware * update source modifying * undo extra change
This commit is contained in:
parent
672736c408
commit
9d22da476b
19 changed files with 774 additions and 22 deletions
|
@ -74,12 +74,14 @@ export function getUtils({
|
|||
basePath,
|
||||
rewrites,
|
||||
pageIsDynamic,
|
||||
trailingSlash,
|
||||
}: {
|
||||
page: ServerlessHandlerCtx['page']
|
||||
i18n?: ServerlessHandlerCtx['i18n']
|
||||
basePath: ServerlessHandlerCtx['basePath']
|
||||
rewrites: ServerlessHandlerCtx['rewrites']
|
||||
pageIsDynamic: ServerlessHandlerCtx['pageIsDynamic']
|
||||
trailingSlash?: boolean
|
||||
}) {
|
||||
let defaultRouteRegex: ReturnType<typeof getNamedRouteRegex> | undefined
|
||||
let dynamicRouteMatcher: RouteMatch | undefined
|
||||
|
@ -107,10 +109,13 @@ export function getUtils({
|
|||
}
|
||||
|
||||
const checkRewrite = (rewrite: Rewrite): boolean => {
|
||||
const matcher = getPathMatch(rewrite.source, {
|
||||
removeUnnamedParams: true,
|
||||
strict: true,
|
||||
})
|
||||
const matcher = getPathMatch(
|
||||
rewrite.source + (trailingSlash ? '(/)?' : ''),
|
||||
{
|
||||
removeUnnamedParams: true,
|
||||
strict: true,
|
||||
}
|
||||
)
|
||||
let params = matcher(parsedUrl.pathname)
|
||||
|
||||
if (rewrite.has && params) {
|
||||
|
|
|
@ -489,7 +489,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
|
||||
if (
|
||||
!isDynamicRoute(srcPathname) &&
|
||||
!(await this.hasPage(srcPathname))
|
||||
!(await this.hasPage(removeTrailingSlash(srcPathname)))
|
||||
) {
|
||||
for (const dynamicRoute of this.dynamicRoutes || []) {
|
||||
if (dynamicRoute.match(srcPathname)) {
|
||||
|
@ -769,6 +769,20 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
let pathname = `/${params.path.join('/')}`
|
||||
pathname = getRouteFromAssetPath(pathname, '.json')
|
||||
|
||||
// ensure trailing slash is normalized per config
|
||||
if (this.router.catchAllMiddleware[0]) {
|
||||
if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) {
|
||||
pathname += '/'
|
||||
}
|
||||
if (
|
||||
!this.nextConfig.trailingSlash &&
|
||||
pathname.length > 1 &&
|
||||
pathname.endsWith('/')
|
||||
) {
|
||||
pathname = pathname.substring(0, pathname.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.nextConfig.i18n) {
|
||||
const { host } = req?.headers || {}
|
||||
// remove port from host and remove port if present
|
||||
|
|
|
@ -1191,13 +1191,15 @@ export default class Router implements BaseRouter {
|
|||
|
||||
// we don't attempt resolve asPath when we need to execute
|
||||
// middleware as the resolving will occur server-side
|
||||
const isMiddlewareMatch =
|
||||
!options.shallow &&
|
||||
(await matchesMiddleware({
|
||||
asPath: as,
|
||||
locale: nextState.locale,
|
||||
router: this,
|
||||
}))
|
||||
const isMiddlewareMatch = await matchesMiddleware({
|
||||
asPath: as,
|
||||
locale: nextState.locale,
|
||||
router: this,
|
||||
})
|
||||
|
||||
if (options.shallow && isMiddlewareMatch) {
|
||||
pathname = this.pathname
|
||||
}
|
||||
|
||||
if (shouldResolveHref && pathname !== '/_error') {
|
||||
;(options as any)._shouldResolveHref = true
|
||||
|
@ -1672,16 +1674,12 @@ export default class Router implements BaseRouter {
|
|||
* for shallow routing purposes.
|
||||
*/
|
||||
let route = requestedRoute
|
||||
|
||||
try {
|
||||
const handleCancelled = getCancelledHandler({ route, router: this })
|
||||
|
||||
let existingInfo: PrivateRouteInfo | undefined = this.components[route]
|
||||
if (
|
||||
!hasMiddleware &&
|
||||
routeProps.shallow &&
|
||||
existingInfo &&
|
||||
this.route === route
|
||||
) {
|
||||
if (routeProps.shallow && existingInfo && this.route === route) {
|
||||
return existingInfo
|
||||
}
|
||||
|
||||
|
|
|
@ -34,10 +34,13 @@ export default function resolveRewrites(
|
|||
let resolvedHref
|
||||
|
||||
const handleRewrite = (rewrite: Rewrite) => {
|
||||
const matcher = getPathMatch(rewrite.source, {
|
||||
removeUnnamedParams: true,
|
||||
strict: true,
|
||||
})
|
||||
const matcher = getPathMatch(
|
||||
rewrite.source + (process.env.__NEXT_TRAILING_SLASH ? '(/)?' : ''),
|
||||
{
|
||||
removeUnnamedParams: true,
|
||||
strict: true,
|
||||
}
|
||||
)
|
||||
|
||||
let params = matcher(parsedAs.pathname)
|
||||
|
||||
|
|
107
test/e2e/middleware-trailing-slash/app/middleware.js
Normal file
107
test/e2e/middleware-trailing-slash/app/middleware.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
/* global URLPattern */
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function middleware(request) {
|
||||
const url = request.nextUrl
|
||||
|
||||
// this is needed for tests to get the BUILD_ID
|
||||
if (url.pathname.startsWith('/_next/static/__BUILD_ID')) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
if (request.headers.get('x-prerender-revalidate')) {
|
||||
return NextResponse.next({
|
||||
headers: { 'x-middleware': 'hi' },
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname === '/about/') {
|
||||
return NextResponse.rewrite(new URL('/about/a', request.url))
|
||||
}
|
||||
|
||||
if (url.pathname === '/ssr-page/') {
|
||||
url.pathname = '/ssr-page-2'
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
if (url.pathname === '/') {
|
||||
url.pathname = '/ssg/first'
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
if (url.pathname === '/to-ssg/') {
|
||||
url.pathname = '/ssg/hello'
|
||||
url.searchParams.set('from', 'middleware')
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
if (url.pathname === '/sha/') {
|
||||
url.pathname = '/shallow'
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
if (url.pathname === '/rewrite-to-dynamic/') {
|
||||
url.pathname = '/blog/from-middleware'
|
||||
url.searchParams.set('some', 'middleware')
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
if (url.pathname === '/rewrite-to-config-rewrite/') {
|
||||
url.pathname = '/rewrite-3'
|
||||
url.searchParams.set('some', 'middleware')
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
if (url.pathname === '/redirect-to-somewhere/') {
|
||||
url.pathname = '/somewhere'
|
||||
return NextResponse.redirect(url, {
|
||||
headers: {
|
||||
'x-redirect-header': 'hi',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const original = new URL(request.url)
|
||||
return NextResponse.next({
|
||||
headers: {
|
||||
'req-url-path': `${original.pathname}${original.search}`,
|
||||
'req-url-basepath': request.nextUrl.basePath,
|
||||
'req-url-pathname': request.nextUrl.pathname,
|
||||
'req-url-query': request.nextUrl.searchParams.get('foo'),
|
||||
'req-url-locale': request.nextUrl.locale,
|
||||
'req-url-params':
|
||||
url.pathname !== '/static' ? JSON.stringify(params(request.url)) : '{}',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const PATTERNS = [
|
||||
[
|
||||
new URLPattern({ pathname: '/:locale/:id' }),
|
||||
({ pathname }) => ({
|
||||
pathname: '/:locale/:id',
|
||||
params: pathname.groups,
|
||||
}),
|
||||
],
|
||||
[
|
||||
new URLPattern({ pathname: '/:id' }),
|
||||
({ pathname }) => ({
|
||||
pathname: '/:id',
|
||||
params: pathname.groups,
|
||||
}),
|
||||
],
|
||||
]
|
||||
|
||||
const params = (url) => {
|
||||
const input = url.split('?')[0]
|
||||
let result = {}
|
||||
|
||||
for (const [pattern, handler] of PATTERNS) {
|
||||
const patternResult = pattern.exec(input)
|
||||
if (patternResult !== null && 'pathname' in patternResult) {
|
||||
result = handler(patternResult)
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
32
test/e2e/middleware-trailing-slash/app/next.config.js
Normal file
32
test/e2e/middleware-trailing-slash/app/next.config.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
module.exports = {
|
||||
trailingSlash: true,
|
||||
redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/redirect-1',
|
||||
destination: '/somewhere/else/',
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/rewrite-1',
|
||||
destination: '/ssr-page?from=config',
|
||||
},
|
||||
{
|
||||
source: '/rewrite-2',
|
||||
destination: '/about/a?from=next-config',
|
||||
},
|
||||
{
|
||||
source: '/sha',
|
||||
destination: '/shallow',
|
||||
},
|
||||
{
|
||||
source: '/rewrite-3',
|
||||
destination: '/blog/middleware-rewrite?hello=config',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
3
test/e2e/middleware-trailing-slash/app/pages/[id].js
Normal file
3
test/e2e/middleware-trailing-slash/app/pages/[id].js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Index() {
|
||||
return <p className="title">Dynamic route</p>
|
||||
}
|
8
test/e2e/middleware-trailing-slash/app/pages/_app.js
Normal file
8
test/e2e/middleware-trailing-slash/app/pages/_app.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default function App({ Component, pageProps }) {
|
||||
if (!pageProps || typeof pageProps !== 'object') {
|
||||
throw new Error(
|
||||
`Invariant: received invalid pageProps in _app, received ${pageProps}`
|
||||
)
|
||||
}
|
||||
return <Component {...pageProps} />
|
||||
}
|
7
test/e2e/middleware-trailing-slash/app/pages/about/a.js
Normal file
7
test/e2e/middleware-trailing-slash/app/pages/about/a.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function AboutA() {
|
||||
return (
|
||||
<div>
|
||||
<h1>AboutA</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
7
test/e2e/middleware-trailing-slash/app/pages/about/b.js
Normal file
7
test/e2e/middleware-trailing-slash/app/pages/about/b.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function AboutB() {
|
||||
return (
|
||||
<div>
|
||||
<h1>AboutB</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function handler(req, res) {
|
||||
res.json({ url: req.url, headers: req.headers })
|
||||
}
|
23
test/e2e/middleware-trailing-slash/app/pages/blog/[slug].js
Normal file
23
test/e2e/middleware-trailing-slash/app/pages/blog/[slug].js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Page(props) {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<>
|
||||
<p id="blog">/blog/[slug]</p>
|
||||
<p id="query">{JSON.stringify(router.query)}</p>
|
||||
<p id="pathname">{router.pathname}</p>
|
||||
<p id="as-path">{router.asPath}</p>
|
||||
<p id="props">{JSON.stringify(props)}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function getServerSideProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
now: Date.now(),
|
||||
params,
|
||||
},
|
||||
}
|
||||
}
|
12
test/e2e/middleware-trailing-slash/app/pages/error-throw.js
Normal file
12
test/e2e/middleware-trailing-slash/app/pages/error-throw.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default function ThrowOnData({ message }) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">Throw on data request</h1>
|
||||
<p className={message}>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = ({ query }) => ({
|
||||
props: { message: query.message || '' },
|
||||
})
|
11
test/e2e/middleware-trailing-slash/app/pages/error.js
Normal file
11
test/e2e/middleware-trailing-slash/app/pages/error.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default function Errors() {
|
||||
return (
|
||||
<div>
|
||||
<Link href="/error-throw?message=refreshed">
|
||||
<a id="throw-on-data">Throw on data</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
43
test/e2e/middleware-trailing-slash/app/pages/shallow.js
Normal file
43
test/e2e/middleware-trailing-slash/app/pages/shallow.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Shallow({ message }) {
|
||||
const { pathname, query } = useRouter()
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
<li id="message-contents">{message}</li>
|
||||
<li>
|
||||
<Link href="/sha?hello=world" shallow>
|
||||
<a id="shallow-link">Shallow link to ?hello=world</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/sha?hello=goodbye">
|
||||
<a id="deep-link">Deep link to ?hello=goodbye</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<h1 id="pathname">
|
||||
Current path: <code>{pathname}</code>
|
||||
</h1>
|
||||
</li>
|
||||
<li>
|
||||
<h2 id="query" data-query-hello={query.hello}>
|
||||
Current query: <code>{JSON.stringify(query)}</code>
|
||||
</h2>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let i = 0
|
||||
|
||||
export const getServerSideProps = () => {
|
||||
return {
|
||||
props: {
|
||||
message: `Random: ${++i}${Math.random()}`,
|
||||
},
|
||||
}
|
||||
}
|
42
test/e2e/middleware-trailing-slash/app/pages/ssg/[slug].js
Normal file
42
test/e2e/middleware-trailing-slash/app/pages/ssg/[slug].js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Page(props) {
|
||||
const router = useRouter()
|
||||
const [asPath, setAsPath] = useState(
|
||||
router.isReady ? router.asPath : router.href
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
setAsPath(router.asPath)
|
||||
}
|
||||
}, [router.asPath, router.isReady])
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="ssg">/blog/[slug]</p>
|
||||
<p id="query">{JSON.stringify(router.query)}</p>
|
||||
<p id="pathname">{router.pathname}</p>
|
||||
<p id="as-path">{asPath}</p>
|
||||
<p id="props">{JSON.stringify(props)}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function getStaticProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
now: Date.now(),
|
||||
params,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getStaticPaths() {
|
||||
return {
|
||||
paths: ['/ssg/first', '/ssg/hello'],
|
||||
fallback: 'blocking',
|
||||
}
|
||||
}
|
11
test/e2e/middleware-trailing-slash/app/pages/ssr-page-2.js
Normal file
11
test/e2e/middleware-trailing-slash/app/pages/ssr-page-2.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default function SSRPage(props) {
|
||||
return <h1>{props.message}</h1>
|
||||
}
|
||||
|
||||
export const getServerSideProps = (req) => {
|
||||
return {
|
||||
props: {
|
||||
message: 'Bye Cruel World',
|
||||
},
|
||||
}
|
||||
}
|
11
test/e2e/middleware-trailing-slash/app/pages/ssr-page.js
Normal file
11
test/e2e/middleware-trailing-slash/app/pages/ssr-page.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default function SSRPage(props) {
|
||||
return <h1>{props.message}</h1>
|
||||
}
|
||||
|
||||
export const getServerSideProps = (req) => {
|
||||
return {
|
||||
props: {
|
||||
message: 'Hello World',
|
||||
},
|
||||
}
|
||||
}
|
412
test/e2e/middleware-trailing-slash/test/index.test.ts
Normal file
412
test/e2e/middleware-trailing-slash/test/index.test.ts
Normal file
|
@ -0,0 +1,412 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import { check, fetchViaHTTP, waitFor } from 'next-test-utils'
|
||||
|
||||
describe('Middleware Runtime trailing slash', () => {
|
||||
let next: NextInstance
|
||||
|
||||
afterAll(async () => {
|
||||
await next.destroy()
|
||||
})
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
|
||||
'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
|
||||
pages: new FileRef(join(__dirname, '../app/pages')),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
function runTests() {
|
||||
if ((global as any).isNextDev) {
|
||||
it('refreshes the page when middleware changes ', async () => {
|
||||
const browser = await webdriver(next.url, `/about/`)
|
||||
await browser.eval('window.didrefresh = "hello"')
|
||||
const text = await browser.elementByCss('h1').text()
|
||||
expect(text).toEqual('AboutA')
|
||||
|
||||
const middlewarePath = join(next.testDir, '/middleware.js')
|
||||
const originalContent = fs.readFileSync(middlewarePath, 'utf-8')
|
||||
const editedContent = originalContent.replace('/about/a', '/about/b')
|
||||
|
||||
try {
|
||||
fs.writeFileSync(middlewarePath, editedContent)
|
||||
await waitFor(1000)
|
||||
const textb = await browser.elementByCss('h1').text()
|
||||
expect(await browser.eval('window.itdidnotrefresh')).not.toBe('hello')
|
||||
expect(textb).toEqual('AboutB')
|
||||
} finally {
|
||||
fs.writeFileSync(middlewarePath, originalContent)
|
||||
await browser.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ((global as any).isNextStart) {
|
||||
it('should have valid middleware field in manifest', async () => {
|
||||
const manifest = await fs.readJSON(
|
||||
join(next.testDir, '.next/server/middleware-manifest.json')
|
||||
)
|
||||
expect(manifest.middleware).toEqual({
|
||||
'/': {
|
||||
files: ['server/edge-runtime-webpack.js', 'server/middleware.js'],
|
||||
name: 'middleware',
|
||||
env: [],
|
||||
page: '/',
|
||||
regexp: '^/.*$',
|
||||
wasm: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should have correct files in manifest', async () => {
|
||||
const manifest = await fs.readJSON(
|
||||
join(next.testDir, '.next/server/middleware-manifest.json')
|
||||
)
|
||||
for (const key of Object.keys(manifest.middleware)) {
|
||||
const middleware = manifest.middleware[key]
|
||||
expect(middleware.files).toContainEqual(
|
||||
expect.stringContaining('server/edge-runtime-webpack')
|
||||
)
|
||||
expect(middleware.files).not.toContainEqual(
|
||||
expect.stringContaining('static/chunks/')
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not run middleware for on-demand revalidate', async () => {
|
||||
const bypassToken = (
|
||||
await fs.readJSON(join(next.testDir, '.next/prerender-manifest.json'))
|
||||
).preview.previewModeId
|
||||
|
||||
const res = await fetchViaHTTP(next.url, '/ssg/first/', undefined, {
|
||||
headers: {
|
||||
'x-prerender-revalidate': bypassToken,
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-middleware')).toBeFalsy()
|
||||
expect(res.headers.get('x-nextjs-cache')).toBe('REVALIDATED')
|
||||
})
|
||||
}
|
||||
|
||||
it('should have init header for NextResponse.redirect', async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/redirect-to-somewhere/',
|
||||
undefined,
|
||||
{
|
||||
redirect: 'manual',
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(307)
|
||||
expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
|
||||
'/somewhere/'
|
||||
)
|
||||
expect(res.headers.get('x-redirect-header')).toBe('hi')
|
||||
})
|
||||
|
||||
it('should have correct query values for rewrite to ssg page', async () => {
|
||||
const browser = await webdriver(next.url, '/to-ssg/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
|
||||
await check(() => browser.elementByCss('body').text(), /\/to-ssg/)
|
||||
|
||||
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
||||
slug: 'hello',
|
||||
from: 'middleware',
|
||||
})
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: 'hello',
|
||||
})
|
||||
expect(await browser.elementByCss('#pathname').text()).toBe('/ssg/[slug]')
|
||||
expect(await browser.elementByCss('#as-path').text()).toBe('/to-ssg/')
|
||||
})
|
||||
|
||||
it('should have correct dynamic route params on client-transition to dynamic route', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.eval('window.next.router.push("/blog/first")')
|
||||
await browser.waitForElementByCss('#blog')
|
||||
|
||||
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
||||
slug: 'first',
|
||||
})
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: 'first',
|
||||
})
|
||||
expect(await browser.elementByCss('#pathname').text()).toBe(
|
||||
'/blog/[slug]'
|
||||
)
|
||||
expect(await browser.elementByCss('#as-path').text()).toBe('/blog/first/')
|
||||
|
||||
await browser.eval('window.next.router.push("/blog/second")')
|
||||
await check(() => browser.elementByCss('body').text(), /"slug":"second"/)
|
||||
|
||||
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
||||
slug: 'second',
|
||||
})
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: 'second',
|
||||
})
|
||||
expect(await browser.elementByCss('#pathname').text()).toBe(
|
||||
'/blog/[slug]'
|
||||
)
|
||||
expect(await browser.elementByCss('#as-path').text()).toBe(
|
||||
'/blog/second/'
|
||||
)
|
||||
})
|
||||
|
||||
it('should have correct dynamic route params for middleware rewrite to dynamic route', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.eval('window.next.router.push("/rewrite-to-dynamic")')
|
||||
await browser.waitForElementByCss('#blog')
|
||||
|
||||
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
||||
slug: 'from-middleware',
|
||||
some: 'middleware',
|
||||
})
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: 'from-middleware',
|
||||
})
|
||||
expect(await browser.elementByCss('#pathname').text()).toBe(
|
||||
'/blog/[slug]'
|
||||
)
|
||||
expect(await browser.elementByCss('#as-path').text()).toBe(
|
||||
'/rewrite-to-dynamic/'
|
||||
)
|
||||
})
|
||||
|
||||
it('should have correct route params for chained rewrite from middleware to config rewrite', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.eval(
|
||||
'window.next.router.push("/rewrite-to-config-rewrite")'
|
||||
)
|
||||
await browser.waitForElementByCss('#blog')
|
||||
|
||||
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
||||
slug: 'middleware-rewrite',
|
||||
hello: 'config',
|
||||
some: 'middleware',
|
||||
})
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: 'middleware-rewrite',
|
||||
})
|
||||
expect(await browser.elementByCss('#pathname').text()).toBe(
|
||||
'/blog/[slug]'
|
||||
)
|
||||
expect(await browser.elementByCss('#as-path').text()).toBe(
|
||||
'/rewrite-to-config-rewrite/'
|
||||
)
|
||||
})
|
||||
|
||||
it('should have correct route params for rewrite from config dynamic route', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.eval('window.next.router.push("/rewrite-3")')
|
||||
await browser.waitForElementByCss('#blog')
|
||||
|
||||
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
||||
slug: 'middleware-rewrite',
|
||||
hello: 'config',
|
||||
})
|
||||
expect(
|
||||
JSON.parse(await browser.elementByCss('#props').text()).params
|
||||
).toEqual({
|
||||
slug: 'middleware-rewrite',
|
||||
})
|
||||
expect(await browser.elementByCss('#pathname').text()).toBe(
|
||||
'/blog/[slug]'
|
||||
)
|
||||
expect(await browser.elementByCss('#as-path').text()).toBe('/rewrite-3/')
|
||||
})
|
||||
|
||||
it('should have correct route params for rewrite from config non-dynamic route', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.eval('window.next.router.push("/rewrite-1")')
|
||||
|
||||
await check(
|
||||
() => browser.eval('document.documentElement.innerHTML'),
|
||||
/Hello World/
|
||||
)
|
||||
|
||||
expect(await browser.eval('window.next.router.query')).toEqual({
|
||||
from: 'config',
|
||||
})
|
||||
})
|
||||
|
||||
it('should redirect the same for direct visit and client-transition', async () => {
|
||||
const res = await fetchViaHTTP(next.url, `/redirect-1/`, undefined, {
|
||||
redirect: 'manual',
|
||||
})
|
||||
expect(res.status).toBe(307)
|
||||
expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
|
||||
'/somewhere/else/'
|
||||
)
|
||||
|
||||
const browser = await webdriver(next.url, `/`)
|
||||
await browser.eval(`next.router.push('/redirect-1')`)
|
||||
await check(async () => {
|
||||
const pathname = await browser.eval('location.pathname')
|
||||
return pathname === '/somewhere/else/' ? 'success' : pathname
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('should rewrite the same for direct visit and client-transition', async () => {
|
||||
const res = await fetchViaHTTP(next.url, `/rewrite-1/`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toContain('Hello World')
|
||||
|
||||
const browser = await webdriver(next.url, `/`)
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.eval(`next.router.push('/rewrite-1')`)
|
||||
await check(async () => {
|
||||
const content = await browser.eval('document.documentElement.innerHTML')
|
||||
return content.includes('Hello World') ? 'success' : content
|
||||
}, 'success')
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
})
|
||||
|
||||
it('should rewrite correctly for non-SSG/SSP page', async () => {
|
||||
const res = await fetchViaHTTP(next.url, `/rewrite-2/`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toContain('AboutA')
|
||||
|
||||
const browser = await webdriver(next.url, `/`)
|
||||
await browser.eval(`next.router.push('/rewrite-2')`)
|
||||
await check(async () => {
|
||||
const content = await browser.eval('document.documentElement.innerHTML')
|
||||
return content.includes('AboutA') ? 'success' : content
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('should respond with 400 on decode failure', async () => {
|
||||
const res = await fetchViaHTTP(next.url, `/%2/`)
|
||||
expect(res.status).toBe(400)
|
||||
|
||||
if ((global as any).isNextStart) {
|
||||
expect(await res.text()).toContain('Bad Request')
|
||||
}
|
||||
})
|
||||
|
||||
it(`should validate & parse request url from any route`, async () => {
|
||||
const res = await fetchViaHTTP(next.url, `/static/`)
|
||||
|
||||
expect(res.headers.get('req-url-basepath')).toBeFalsy()
|
||||
expect(res.headers.get('req-url-pathname')).toBe('/static/')
|
||||
|
||||
const { pathname, params } = JSON.parse(res.headers.get('req-url-params'))
|
||||
expect(pathname).toBe(undefined)
|
||||
expect(params).toEqual(undefined)
|
||||
|
||||
expect(res.headers.get('req-url-query')).not.toBe('bar')
|
||||
})
|
||||
|
||||
it('should trigger middleware for data requests', async () => {
|
||||
const browser = await webdriver(next.url, `/ssr-page`)
|
||||
const text = await browser.elementByCss('h1').text()
|
||||
expect(text).toEqual('Bye Cruel World')
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/ssr-page.json`,
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'x-nextjs-data': '1',
|
||||
},
|
||||
}
|
||||
)
|
||||
const json = await res.json()
|
||||
expect(json.pageProps.message).toEqual('Bye Cruel World')
|
||||
})
|
||||
|
||||
it('should normalize data requests into page requests', async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/send-url.json`,
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'x-nextjs-data': '1',
|
||||
},
|
||||
}
|
||||
)
|
||||
expect(res.headers.get('req-url-path')).toEqual('/send-url/')
|
||||
})
|
||||
|
||||
it('should keep non data requests in their original shape', async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/static/${next.buildId}/_devMiddlewareManifest.json?foo=1`
|
||||
)
|
||||
expect(res.headers.get('req-url-path')).toEqual(
|
||||
`/_next/static/${next.buildId}/_devMiddlewareManifest.json?foo=1`
|
||||
)
|
||||
})
|
||||
|
||||
it('should add a rewrite header on data requests for rewrites', async () => {
|
||||
const res = await fetchViaHTTP(next.url, `/ssr-page/`)
|
||||
const dataRes = await fetchViaHTTP(
|
||||
next.url,
|
||||
`/_next/data/${next.buildId}/ssr-page.json`,
|
||||
undefined,
|
||||
{ headers: { 'x-nextjs-data': '1' } }
|
||||
)
|
||||
const json = await dataRes.json()
|
||||
expect(json.pageProps.message).toEqual('Bye Cruel World')
|
||||
expect(res.headers.get('x-nextjs-matched-path')).toBeNull()
|
||||
expect(dataRes.headers.get('x-nextjs-matched-path')).toEqual(
|
||||
`/ssr-page-2`
|
||||
)
|
||||
})
|
||||
|
||||
it('allows shallow linking with middleware', async () => {
|
||||
const browser = await webdriver(next.url, '/sha/')
|
||||
const getMessageContents = () =>
|
||||
browser.elementById('message-contents').text()
|
||||
const ssrMessage = await getMessageContents()
|
||||
const requests: string[] = []
|
||||
|
||||
browser.on('request', (x) => {
|
||||
requests.push(x.url())
|
||||
})
|
||||
|
||||
browser.elementById('deep-link').click()
|
||||
browser.waitForElementByCss('[data-query-hello="goodbye"]')
|
||||
const deepLinkMessage = await getMessageContents()
|
||||
expect(deepLinkMessage).not.toEqual(ssrMessage)
|
||||
|
||||
// Changing the route with a shallow link should not cause a server request
|
||||
browser.elementById('shallow-link').click()
|
||||
browser.waitForElementByCss('[data-query-hello="world"]')
|
||||
expect(await getMessageContents()).toEqual(deepLinkMessage)
|
||||
|
||||
// Check that no server requests were made to ?hello=world,
|
||||
// as it's a shallow request.
|
||||
expect(requests.filter((req) => req.includes('_next/data'))).toEqual([
|
||||
`${next.url}/_next/data/${next.buildId}/sha.json?hello=goodbye`,
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
runTests()
|
||||
})
|
Loading…
Reference in a new issue