Files
next.js/test/integration/repeated-slashes/test/index.test.ts
Niklas MischkulnigandGitHub b2f193b096 test: Cleanup and refactoring (#90454)
- `expect(hasImageMatchingUrl(browser, "...")).toBe(true)` has very bad error messages if the assertion fails
- there was a `(process.env.TURBOPACK_DEV ? describe.skip : describe)('production mode',` around a dev test
2026-02-24 20:03:44 +01:00

495 lines
15 KiB
TypeScript

/* eslint-env jest */
import { join } from 'path'
import fs from 'fs-extra'
import webdriver from 'next-webdriver'
import escapeRegex from 'escape-string-regexp'
import {
nextBuild,
File,
findPort,
nextStart,
killApp,
waitFor,
stopApp,
startStaticServer,
launchApp,
fetchViaHTTP,
check,
renderViaHTTP,
} from 'next-test-utils'
const appDir = join(__dirname, '../app')
const outdir = join(appDir, 'out')
const nextConfig = new File(join(appDir, 'next.config.js'))
let appPort
let app
function runTests({ isDev = false, isExport = false, isPages404 = false }) {
let notFoundContent = 'custom error'
// let badRequestContent = 'custom error'
if (isPages404) {
// badRequestContent = 'Bad Request'
notFoundContent = 'custom 404'
}
if (isExport && isPages404) {
notFoundContent = 'custom 404'
// badRequestContent = 'custom 404'
}
const didNotReload = async (browser) => {
for (let i = 0; i < 4; i++) {
await waitFor(500)
const result = await browser.eval('window.errorLoad')
if (result !== true) {
throw new Error(
`did not find window.errorLoad, current url: ${await browser.url()}`
)
}
if (isDev) break
}
}
if (!isExport) {
it('should normalize repeated slashes in redirects correctly', async () => {
const res = await fetchViaHTTP(
appPort,
'/redirect-forward-slashes',
undefined,
{
redirect: 'manual',
}
)
expect(res.status).toBe(307)
const parsedUrl = new URL(res.headers.get('location'))
expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1'])
expect(parsedUrl.pathname).toBe('/test/google.com')
expect(Object.fromEntries(parsedUrl.searchParams.entries())).toEqual({})
expect(await res.text()).toBe('/test/google.com')
const res2 = await fetchViaHTTP(
appPort,
'/redirect-back-slashes',
undefined,
{
redirect: 'manual',
}
)
expect(res2.status).toBe(307)
const parsedUrl2 = new URL(res2.headers.get('location'))
expect(parsedUrl2.hostname).toBeOneOf(['localhost', '127.0.0.1'])
expect(parsedUrl2.pathname).toBe('/test/google.com')
expect(Object.fromEntries(parsedUrl2.searchParams.entries())).toEqual({})
expect(await res2.text()).toBe('/test/google.com')
})
}
it('should handle double slashes correctly', async () => {
if (!isExport) {
const res = await fetchViaHTTP(appPort, '//google.com', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(308)
const parsedUrl = new URL(res.headers.get('location'))
expect(parsedUrl.pathname).toBe('/google.com')
expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1'])
expect(Object.fromEntries(parsedUrl.searchParams.entries())).toEqual({})
}
const browser = await webdriver(appPort, '//google.com')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
isExport ? '//google.com' : '/google.com'
)
expect(await browser.eval('window.location.search')).toBe('')
expect(await browser.eval('window.location.hash')).toBe('')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle double slashes correctly with query', async () => {
if (!isExport) {
const res = await fetchViaHTTP(
appPort,
'//google.com',
{ h: '1' },
{
redirect: 'manual',
}
)
expect(res.status).toBe(308)
const parsedUrl = new URL(res.headers.get('location'))
expect(parsedUrl.pathname).toBe('/google.com')
expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1'])
expect(Object.fromEntries(parsedUrl.searchParams.entries())).toEqual({
h: '1',
})
}
const browser = await webdriver(appPort, '//google.com?h=1')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
isExport ? '//google.com' : '/google.com'
)
expect(await browser.eval('window.location.search')).toBe('?h=1')
expect(await browser.eval('window.location.hash')).toBe('')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle double slashes correctly with hash', async () => {
if (!isExport) {
const res = await fetchViaHTTP(appPort, '//google.com', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(308)
const parsedUrl = new URL(res.headers.get('location'))
expect(parsedUrl.pathname).toBe('/google.com')
expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1'])
expect(Object.fromEntries(parsedUrl.searchParams.entries())).toEqual({})
}
const browser = await webdriver(appPort, '//google.com#hello')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
isExport ? '//google.com' : '/google.com'
)
expect(await browser.eval('window.location.hash')).toBe('#hello')
expect(await browser.eval('window.location.search')).toBe('')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle double slashes correctly with encoded', async () => {
if (!isExport) {
const res = await fetchViaHTTP(appPort, '/%2Fgoogle.com', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(404)
expect(await res.text()).toContain(notFoundContent)
}
const browser = await webdriver(appPort, '/%2Fgoogle.com')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
'/%2Fgoogle.com'
)
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle double slashes correctly with encoded and query', async () => {
if (!isExport) {
const res = await fetchViaHTTP(
appPort,
'/%2Fgoogle.com',
{ hello: '1' },
{
redirect: 'manual',
}
)
expect(res.status).toBe(404)
expect(await res.text()).toContain(notFoundContent)
}
const browser = await webdriver(appPort, '/%2Fgoogle.com?hello=1')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
'/%2Fgoogle.com'
)
expect(await browser.eval('window.location.search')).toBe('?hello=1')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle double slashes correctly with encoded and hash', async () => {
if (!isExport) {
const res = await fetchViaHTTP(appPort, '/%2Fgoogle.com', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(404)
expect(await res.text()).toContain(notFoundContent)
}
const browser = await webdriver(appPort, '/%2Fgoogle.com#hello')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
'/%2Fgoogle.com'
)
expect(await browser.eval('window.location.hash')).toBe('#hello')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle backslashes correctly', async () => {
if (!isExport) {
const res = await fetchViaHTTP(appPort, '/\\google.com', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(308)
const parsedUrl = new URL(res.headers.get('location'))
expect(parsedUrl.pathname).toBe('/google.com')
expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1'])
expect(Object.fromEntries(parsedUrl.searchParams.entries())).toEqual({})
expect(await res.text()).toBe('/google.com')
}
const browser = await webdriver(appPort, '/\\google.com')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
isExport ? '//google.com' : '/google.com'
)
expect(await browser.eval('window.location.hash')).toBe('')
expect(await browser.eval('window.location.search')).toBe('')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle mixed backslashes/forward slashes correctly', async () => {
if (!isExport) {
const res = await fetchViaHTTP(appPort, '/\\/google.com', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(308)
const parsedUrl = new URL(res.headers.get('location'))
expect(parsedUrl.pathname).toBe(isExport ? '//google.com' : '/google.com')
expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1'])
expect(Object.fromEntries(parsedUrl.searchParams.entries())).toEqual({})
expect(await res.text()).toBe('/google.com')
}
const browser = await webdriver(appPort, '/\\/google.com#hello')
await didNotReload(browser)
expect(await browser.eval('window.location.pathname')).toBe(
isExport ? '///google.com' : '/google.com'
)
expect(await browser.eval('window.location.hash')).toBe('#hello')
expect(await browser.eval('window.location.search')).toBe('')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
notFoundContent
)
})
it('should handle slashes in next/link correctly', async () => {
const browser = await webdriver(
appPort,
`/invalid${isExport ? '.html' : ''}`
)
const invalidHrefs = [
'//google.com',
'//google.com?hello=1',
'//google.com#hello',
'\\/\\/google.com',
'\\/\\/google.com?hello=1',
'\\/\\/google.com#hello',
]
for (const href of invalidHrefs) {
await check(
() =>
browser.eval(
'window.caughtErrors.map(err => typeof err !== "string" ? err.message : err).join(", ")'
),
new RegExp(escapeRegex(`Invalid href '${href}'`))
)
}
})
it('should handle slashes in router push correctly', async () => {
for (const item of [
{
page: '/another',
href: '/another',
as: '//google.com',
pathname: '/google.com',
},
{
page: isPages404 ? '/404' : '/_error',
href: '//google.com',
pathname: '/google.com',
},
{
page: isPages404 ? '/404' : '/_error',
href: '//google.com?hello=1',
pathname: '/google.com',
search: '?hello=1',
},
{
page: isPages404 ? '/404' : '/_error',
href: '//google.com#hello',
pathname: '/google.com',
hash: '#hello',
},
]) {
const browser = await webdriver(appPort, '/')
await browser.eval(
`window.next.router.push("${item.href}"${
item.as ? `, "${item.as}"` : ''
})`
)
await check(
() => browser.eval('document.readyState'),
/interactive|complete/
)
expect(await browser.eval('window.location.pathname')).toBe(item.pathname)
expect(await browser.eval('window.location.search')).toBe(
item.search || ''
)
expect(await browser.eval('window.location.hash')).toBe(item.hash || '')
expect(await browser.eval('window.next.router.pathname')).toBe(item.page)
}
})
it('should have no error from encoded slashes in router push', async () => {
for (const item of [
{
page: '/another',
href: '/another',
as: '/%2Fgoogle.com',
shouldHardNav: false,
},
{
page: isPages404 ? '/404' : '/_error',
href: '/%2Fgoogle.com',
},
{
page: isPages404 ? '/404' : '/_error',
href: '/%2F%2Fgoogle.com?hello=1',
pathname: '/%2F%2Fgoogle.com',
search: '?hello=1',
},
{
page: isPages404 ? '/404' : '/_error',
href: '/%5C%5C%2Fgoogle.com#hello',
pathname: '/%5C%5C%2Fgoogle.com',
hash: '#hello',
},
]) {
const browser = await webdriver(appPort, '/')
await browser.eval(`(function() {
window.beforeNav = 1
window.next.router.push("${item.href}"${
item.as ? `, "${item.as}"` : ''
})
})()`)
await check(
() => browser.eval('window.location.pathname'),
item.pathname || item.as || item.href
)
expect(await browser.eval('window.location.search')).toBe(
item.search || ''
)
expect(await browser.eval('window.location.hash')).toBe(item.hash || '')
expect(await browser.eval('window.next.router.pathname')).toBe(item.page)
expect(await browser.eval('window.next.router.asPath')).toBe(
item.as || item.href
)
if (item.shouldHardNav !== false) {
expect(await browser.eval('window.beforeNav')).toBeFalsy()
} else {
expect(await browser.eval('window.beforeNav')).toBe(1)
}
}
})
}
describe('404 handling', () => {
const devStartAndExport = (isPages404) => {
describe('next dev', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
// prebuild pages
await renderViaHTTP(appPort, '/')
await renderViaHTTP(appPort, '/another')
})
afterAll(() => killApp(app))
runTests({
isPages404,
isDev: true,
})
})
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
'production mode',
() => {
describe('next start', () => {
beforeAll(async () => {
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(() => killApp(app))
runTests({
isPages404,
})
})
describe('next export', () => {
beforeAll(async () => {
nextConfig.write(`module.exports = { output: 'export' }`)
await nextBuild(appDir)
app = await startStaticServer(outdir, join(outdir, '404.html'))
appPort = app.address().port
})
afterAll(async () => {
await stopApp(app)
nextConfig.restore()
})
runTests({
isPages404,
isExport: true,
})
})
}
)
}
describe('custom _error', () => {
devStartAndExport(false)
})
describe('pages/404', () => {
const pagesErr = join(appDir, 'pages/_error.js')
const pages404 = join(appDir, 'pages/404.js')
beforeAll(async () => {
await fs.move(pagesErr, pagesErr + '.bak')
await fs.writeFile(
pages404,
`
if (typeof window !== 'undefined') {
window.errorLoad = true
}
export default function Page() {
return <p id='error'>custom 404</p>
}
`
)
})
afterAll(async () => {
await fs.move(pagesErr + '.bak', pagesErr)
await fs.remove(pages404)
})
devStartAndExport(true)
})
})