first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,578 @@
import assert from 'assert'
import cheerio from 'cheerio'
import webdriver from 'next-webdriver'
import { nextTestSetup } from 'e2e-utils'
import {
waitForNoRedbox,
check,
fetchViaHTTP,
getClientBuildManifestLoaderChunkUrlPath,
renderViaHTTP,
waitFor,
} from 'next-test-utils'
describe('basePath', () => {
const basePath = '/docs'
const { next, isNextDev, isNextDeploy } = nextTestSetup({
files: __dirname,
nextConfig: {
basePath,
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},
async rewrites() {
return [
{
source: '/rewrite-no-basepath',
destination: 'https://example.vercel.sh',
basePath: false,
},
]
},
async headers() {
return [
{
source: '/add-header',
headers: [
{
key: 'x-hello',
value: 'world',
},
],
},
{
source: '/add-header-no-basepath',
basePath: false,
headers: [
{
key: 'x-hello',
value: 'world',
},
],
},
]
},
},
})
it('should navigate to external site and back', async () => {
const browser = await webdriver(next.url, `${basePath}/external-and-back`)
const initialText = await browser.elementByCss('p').text()
expect(initialText).toBe('server')
await browser
.elementByCss('a')
.click()
.waitForElementByCss('input', { state: 'attached' })
.back()
.waitForElementByCss('p')
await waitFor(1000)
const newText = await browser.elementByCss('p').text()
expect(newText).toBe('server')
})
if (process.env.BROWSER_NAME === 'safari') {
// currently only testing the above test in safari
// we can investigate testing more cases below if desired
return
}
it('should navigate back correctly to a dynamic route', async () => {
const browser = await webdriver(next.url, `${basePath}`)
expect(await browser.elementByCss('#index-page').text()).toContain(
'index page'
)
await browser.eval('window.beforeNav = 1')
await browser.eval('window.next.router.push("/catchall/first")')
await check(() => browser.elementByCss('p').text(), /first/)
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.eval('window.next.router.push("/catchall/second")')
await check(() => browser.elementByCss('p').text(), /second/)
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.eval('window.next.router.back()')
await check(() => browser.elementByCss('p').text(), /first/)
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.eval('window.history.forward()')
await check(() => browser.elementByCss('p').text(), /second/)
expect(await browser.eval('window.beforeNav')).toBe(1)
})
if (!isNextDev) {
if (!isNextDeploy) {
it('should add basePath to routes-manifest', async () => {
const routesManifest = JSON.parse(
await next.readFile('.next/routes-manifest.json')
)
expect(routesManifest.basePath).toBe(basePath)
})
it('should prefetch pages correctly when manually called', async () => {
const browser = await webdriver(next.url, `${basePath}/other-page`)
await browser.eval('window.next.router.prefetch("/gssp")')
let chunk = getClientBuildManifestLoaderChunkUrlPath(
next.testDir,
'/gssp'
)
await check(async () => {
const links = await browser.elementsByCss('link[rel=prefetch]')
for (const link of links) {
const href = await link.getAttribute('href')
if (href.includes(chunk)) {
return true
}
}
const scripts = await browser.elementsByCss('script')
for (const script of scripts) {
const src = await script.getAttribute('src')
if (src.includes(chunk)) {
return true
}
}
return false
}, true)
})
it('should prefetch pages correctly in viewport with <Link>', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.next.router.prefetch("/gssp")')
await check(async () => {
const hrefs = await browser.eval(
`Object.keys(window.next.router.sdc)`
)
hrefs.sort()
assert.deepEqual(
hrefs.map((href) =>
new URL(href).pathname.replace(/\/_next\/data\/[^/]+/, '')
),
[
`${basePath}/gsp.json`,
`${basePath}/index.json`,
// `${basePath}/index/index.json`,
]
)
let chunkGsp = getClientBuildManifestLoaderChunkUrlPath(
next.testDir,
'/gsp'
)
let chunkGssp = getClientBuildManifestLoaderChunkUrlPath(
next.testDir,
'/gssp'
)
let chunkOtherPage = getClientBuildManifestLoaderChunkUrlPath(
next.testDir,
'/other-page'
)
const prefetches = await browser.eval(
`[].slice.call(document.querySelectorAll("link[rel=prefetch]")).map((e) => new URL(e.href).pathname)`
)
expect(prefetches).toContainEqual(expect.stringContaining(chunkGsp))
expect(prefetches).toContainEqual(expect.stringContaining(chunkGssp))
expect(prefetches).toContainEqual(
expect.stringContaining(chunkOtherPage)
)
return 'yes'
}, 'yes')
})
}
}
it('should serve public file with basePath correctly', async () => {
const res = await fetchViaHTTP(next.url, `${basePath}/data.txt`)
expect(res.status).toBe(200)
expect(await res.text()).toBe('hello world')
})
it('should 404 for public file without basePath', async () => {
const res = await fetchViaHTTP(next.url, '/data.txt')
expect(res.status).toBe(404)
})
it('should add header with basePath by default', async () => {
const res = await fetchViaHTTP(next.url, `${basePath}/add-header`)
expect(res.headers.get('x-hello')).toBe('world')
})
it('should not add header without basePath without disabling', async () => {
const res = await fetchViaHTTP(next.url, '/add-header')
expect(res.headers.get('x-hello')).toBe(null)
})
it('should not add header with basePath when set to false', async () => {
const res = await fetchViaHTTP(
next.url,
`${basePath}/add-header-no-basepath`
)
expect(res.headers.get('x-hello')).toBe(null)
})
it('should add header without basePath when set to false', async () => {
const res = await fetchViaHTTP(next.url, '/add-header-no-basepath')
expect(res.headers.get('x-hello')).toBe('world')
})
it('should update dynamic params after mount correctly', async () => {
const browser = await webdriver(next.url, `${basePath}/hello-dynamic`)
await check(
() => browser.elementByCss('#slug').text(),
/slug: hello-dynamic/
)
})
it('should navigate to index page with getStaticProps', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = "hi"')
await browser.elementByCss('#index-gsp').click()
await browser.waitForElementByCss('#prop')
expect(await browser.eval('window.beforeNavigate')).toBe('hi')
expect(await browser.elementByCss('#prop').text()).toBe('hello world')
expect(await browser.elementByCss('#nested').text()).toBe('no')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({})
expect(await browser.elementByCss('#pathname').text()).toBe('/')
if (!isNextDev) {
const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`)
hrefs.sort()
expect(
hrefs.map((href) =>
new URL(href).pathname.replace(/\/_next\/data\/[^/]+/, '')
)
).toEqual([
`${basePath}/gsp.json`,
`${basePath}/index.json`,
// `${basePath}/index/index.json`,
])
}
})
// TODO: investigate index/index seems this shouldn't work
// as pages/index.js conflicts with pages/index/index.js
it.skip('should navigate to nested index page with getStaticProps', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = "hi"')
await browser.elementByCss('#nested-index-gsp').click()
await browser.waitForElementByCss('#prop')
expect(await browser.eval('window.beforeNavigate')).toBe('hi')
expect(await browser.elementByCss('#prop').text()).toBe('hello world')
expect(await browser.elementByCss('#nested').text()).toBe('yes')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({})
expect(await browser.elementByCss('#pathname').text()).toBe('/index')
if (!isNextDev) {
const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`)
hrefs.sort()
expect(
hrefs.map((href) =>
new URL(href).pathname.replace(/\/_next\/data\/[^/]+/, '')
)
).toEqual([
`${basePath}/gsp.json`,
`${basePath}/index.json`,
`${basePath}/index/index.json`,
])
}
})
it('should work with nested folder with same name as basePath', async () => {
const html = await renderViaHTTP(next.url, `${basePath}/docs/another`)
expect(html).toContain('hello from another')
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.next.router.push("/docs/another")')
await check(() => browser.elementByCss('p').text(), /hello from another/)
})
it('should work with normal dynamic page', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.elementByCss('#dynamic-link').click()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
/slug: first/
)
})
it('should work with catch-all page', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.elementByCss('#catchall-link').click()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
/parts: hello\/world/
)
})
it('should redirect trailing slash correctly', async () => {
const res = await fetchViaHTTP(
next.url,
`${basePath}/hello/`,
{},
{ redirect: 'manual' }
)
expect(res.status).toBe(308)
const { pathname } = new URL(res.headers.get('location'))
expect(pathname).toBe(`${basePath}/hello`)
const text = await res.text()
expect(text).toContain(`${basePath}/hello`)
})
it('should redirect trailing slash on root correctly', async () => {
const res = await fetchViaHTTP(
next.url,
`${basePath}/`,
{},
{ redirect: 'manual' }
)
expect(res.status).toBe(308)
const { pathname } = new URL(res.headers.get('location'))
expect(pathname).toBe(`${basePath}`)
const text = await res.text()
expect(text).toContain(`${basePath}`)
})
it('should navigate an absolute url', async () => {
const browser = await webdriver(next.url, `${basePath}/absolute-url`)
await browser.waitForElementByCss('#absolute-link').click()
await check(
() => browser.eval(() => window.location.origin),
'https://vercel.com'
)
})
if (!isNextDeploy) {
it('should navigate an absolute local url with basePath', async () => {
const browser = await webdriver(
next.url,
`${basePath}/absolute-url-basepath?port=${next.appPort}`
)
await browser.eval('window._didNotNavigate = true')
await browser.waitForElementByCss('#absolute-link').click()
const text = await browser
.waitForElementByCss('#something-else-page')
.text()
expect(text).toBe('something else')
expect(await browser.eval('window._didNotNavigate')).toBe(true)
})
it('should navigate an absolute local url without basePath', async () => {
const browser = await webdriver(
next.url,
`${basePath}/absolute-url-no-basepath?port=${next.appPort}`
)
await browser.waitForElementByCss('#absolute-link').click()
await check(
() => browser.eval(() => location.pathname),
'/rewrite-no-basepath'
)
const text = await browser.elementByCss('body').text()
expect(text).toContain('Example Domain')
})
}
it('should show the hello page under the /docs prefix', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
try {
const text = await browser.elementByCss('h1').text()
expect(text).toBe('Hello World')
} finally {
await browser.close()
}
})
it('should have correct router paths on first load of /', async () => {
const browser = await webdriver(next.url, `${basePath}`)
await browser.waitForElementByCss('#pathname')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/')
const asPath = await browser.elementByCss('#as-path').text()
expect(asPath).toBe('/')
})
it('should have correct router paths on first load of /hello', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.waitForElementByCss('#pathname')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/hello')
const asPath = await browser.elementByCss('#as-path').text()
expect(asPath).toBe('/hello')
})
it('should fetch data for getStaticProps without reloading', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = true')
await browser.elementByCss('#gsp-link').click()
await browser.waitForElementByCss('#gsp')
expect(await browser.eval('window.beforeNavigate')).toBe(true)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.hello).toBe('world')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/gsp')
})
it('should fetch data for getServerSideProps without reloading', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = true')
await browser.elementByCss('#gssp-link').click()
await browser.waitForElementByCss('#gssp')
expect(await browser.eval('window.beforeNavigate')).toBe(true)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.hello).toBe('world')
const pathname = await browser.elementByCss('#pathname').text()
const asPath = await browser.elementByCss('#asPath').text()
expect(pathname).toBe('/gssp')
expect(asPath).toBe('/gssp')
})
it('should have correct href for a link', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
const href = await browser.elementByCss('a').getAttribute('href')
const { pathname } = new URL(href, await browser.url())
expect(pathname).toBe(`${basePath}/other-page`)
})
it('should have correct href for a link to /', async () => {
const browser = await webdriver(next.url, `${basePath}/link-to-root`)
const href = await browser.elementByCss('#link-back').getAttribute('href')
const { pathname } = new URL(href, await browser.url())
expect(pathname).toBe(`${basePath}`)
})
it('should show 404 for page not under the /docs prefix', async () => {
const text = await renderViaHTTP(next.url, '/hello')
expect(text).not.toContain('Hello World')
// the custom 404 only shows inside of the basePath so this
// could be a platform default 404 page on deploy
if (!isNextDeploy) {
expect(text).toContain('This page could not be found')
}
})
it('should show the other-page page under the /docs prefix', async () => {
const browser = await webdriver(next.url, `${basePath}/other-page`)
try {
const text = await browser.elementByCss('h1').text()
expect(text).toBe('Hello Other')
} finally {
await browser.close()
}
})
it('should have basePath field on Router', async () => {
const html = await renderViaHTTP(next.url, `${basePath}/hello`)
const $ = cheerio.load(html)
expect($('#base-path').text()).toBe(`${basePath}`)
})
it('should navigate to the page without refresh', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
try {
await browser.eval('window.itdidnotrefresh = "hello"')
const text = await browser
.elementByCss('#other-page-link')
.click()
.waitForElementByCss('#other-page-title')
.elementByCss('h1')
.text()
expect(text).toBe('Hello Other')
expect(await browser.eval('window.itdidnotrefresh')).toBe('hello')
} finally {
await browser.close()
}
})
it('should allow URL query strings without refresh', async () => {
const browser = await webdriver(next.url, `${basePath}/hello?query=true`)
try {
await browser.eval('window.itdidnotrefresh = "hello"')
await new Promise((resolve, reject) => {
// Timeout of EventSource created in setupPing()
// (on-demand-entries-utils.js) is 5000 ms (see #13132, #13560)
setTimeout(resolve, isNextDev ? 10000 : 1000)
})
expect(await browser.eval('window.itdidnotrefresh')).toBe('hello')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/hello')
expect(await browser.eval('window.location.pathname')).toBe(
`${basePath}/hello`
)
expect(await browser.eval('window.location.search')).toBe('?query=true')
if (isNextDev) {
await waitForNoRedbox(browser)
}
} finally {
await browser.close()
}
})
it('should allow URL query strings on index without refresh', async () => {
const browser = await webdriver(next.url, `${basePath}?query=true`)
try {
await browser.eval('window.itdidnotrefresh = "hello"')
await new Promise((resolve, reject) => {
// Timeout of EventSource created in setupPing()
// (on-demand-entries-utils.js) is 5000 ms (see #13132, #13560)
setTimeout(resolve, isNextDev ? 10000 : 1000)
})
expect(await browser.eval('window.itdidnotrefresh')).toBe('hello')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/')
expect(await browser.eval('window.location.pathname')).toBe(basePath)
expect(await browser.eval('window.location.search')).toBe('?query=true')
if (isNextDev) {
await waitForNoRedbox(browser)
}
} finally {
await browser.close()
}
})
it('should correctly replace state when same asPath but different url', async () => {
const browser = await webdriver(next.url, `${basePath}`)
try {
await browser.elementByCss('#hello-link').click()
await browser.waitForElementByCss('#something-else-link')
await browser.elementByCss('#something-else-link').click()
await browser.waitForElementByCss('#something-else-page')
await browser.back()
await browser.waitForElementByCss('#index-page')
await browser.forward()
await browser.waitForElementByCss('#something-else-page')
} finally {
await browser.close()
}
})
})

View File

@@ -0,0 +1,162 @@
import webdriver from 'next-webdriver'
import { nextTestSetup } from 'e2e-utils'
import { check, renderViaHTTP, retry } from 'next-test-utils'
describe('basePath', () => {
const basePath = '/docs'
const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
nextConfig: {
basePath,
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},
},
})
describe('client-side navigation', () => {
it('should navigate to /404 correctly client-side', async () => {
const browser = await webdriver(next.url, `${basePath}/slug-1`)
await check(
() => browser.eval('document.documentElement.innerHTML'),
/slug-1/
)
await browser.eval('next.router.push("/404", "/slug-2")')
await check(
() => browser.eval('document.documentElement.innerHTML'),
/page could not be found/
)
expect(await browser.eval('location.pathname')).toBe(`${basePath}/slug-2`)
})
it('should navigate to /_error correctly client-side', async () => {
const browser = await webdriver(next.url, `${basePath}/slug-1`)
await check(
() => browser.eval('document.documentElement.innerHTML'),
/slug-1/
)
await browser.eval('next.router.push("/_error", "/slug-2")')
await check(
() => browser.eval('document.documentElement.innerHTML'),
/page could not be found/
)
expect(await browser.eval('location.pathname')).toBe(`${basePath}/slug-2`)
})
})
if (process.env.BROWSER_NAME === 'safari') {
// currently only testing the above tests in safari
// we can investigate testing more cases below if desired
return
}
it('should not update URL for a 404', async () => {
const browser = await webdriver(next.url, '/missing')
// the custom 404 only shows inside of the basePath so this
// could be a platform default 404 page on deploy
if (!isNextDeploy) {
const pathname = await browser.eval(() => window.location.pathname)
expect(await browser.eval(() => (window as any).next.router.asPath)).toBe(
'/missing'
)
expect(pathname).toBe('/missing')
}
})
it('should handle 404 urls that start with basePath', async () => {
const browser = await webdriver(next.url, `${basePath}hello`)
// the custom 404 only shows inside of the basePath so this
// could be a platform default 404 page on deploy
if (!isNextDeploy) {
expect(await browser.eval(() => (window as any).next.router.asPath)).toBe(
`${basePath}hello`
)
expect(await browser.eval(() => window.location.pathname)).toBe(
`${basePath}hello`
)
}
})
// TODO: this test has been passing incorrectly since the below check
// wasn't being awaited. We need to investigate if this test is
// correct or not.
it.skip('should navigate back to a non-basepath 404 that starts with basepath', async () => {
const browser = await webdriver(next.url, `${basePath}hello`)
await browser.eval(() => ((window as any).navigationMarker = true))
await browser.eval(() => (window as any).next.router.push('/hello'))
await browser.waitForElementByCss('#pathname')
await browser.back()
await check(
() => browser.eval(() => window.location.pathname),
`${basePath}hello`
)
expect(await browser.eval(() => (window as any).next.router.asPath)).toBe(
`${basePath}hello`
)
expect(await browser.eval(() => (window as any).navigationMarker)).toBe(
true
)
})
describe('manually added basePath in application logic', () => {
it('should 404 when manually adding basePath with <Link>', async () => {
const browser = await webdriver(
next.url,
`${basePath}/invalid-manual-basepath`
)
await browser.eval('window.beforeNav = "hi"')
await browser.elementByCss('#other-page-link').click()
await retry(async () => {
expect(await browser.eval('window.beforeNav')).not.toEqual('hi')
})
await check(
() => browser.eval('document.documentElement.innerHTML'),
/This page could not be found/
)
})
it('should 404 when manually adding basePath with router.push', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.beforeNav = "hi"')
await browser.eval(`window.next.router.push("${basePath}/other-page")`)
await retry(async () => {
expect(await browser.eval('window.beforeNav')).not.toEqual('hi')
})
const html = await browser.eval('document.documentElement.innerHTML')
expect(html).toContain('This page could not be found')
})
it('should 404 when manually adding basePath with router.replace', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.eval('window.beforeNav = "hi"')
await browser.eval(`window.next.router.replace("${basePath}/other-page")`)
await retry(async () => {
expect(await browser.eval('window.beforeNav')).not.toEqual('hi')
})
const html = await browser.eval('document.documentElement.innerHTML')
expect(html).toContain('This page could not be found')
})
})
it('should show 404 for page not under the /docs prefix', async () => {
const text = await renderViaHTTP(next.url, '/hello')
expect(text).not.toContain('Hello World')
// the custom 404 only shows inside of the basePath so this
// could be a platform default 404 page on deploy
if (!isNextDeploy) {
expect(text).toContain('This page could not be found')
}
})
})

5
test/e2e/basepath/external/page.html vendored Normal file
View File

@@ -0,0 +1,5 @@
<html>
<body>
hello from external
</body>
</html>

View File

@@ -0,0 +1,5 @@
import NextError from 'next/error'
export default function Page() {
return <NextError statusCode={404} />
}

View File

@@ -0,0 +1,4 @@
import { useRouter } from 'next/router'
const Page = () => <p id="slug">slug: {useRouter().query.slug}</p>
export default Page

View File

@@ -0,0 +1,52 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
// We use session storage for the event log so that it will survive
// page reloads, which happen for instance during routeChangeError
const EVENT_LOG_KEY = 'router-event-log'
function getEventLog() {
const data = sessionStorage.getItem(EVENT_LOG_KEY)
return data ? JSON.parse(data) : []
}
function clearEventLog() {
sessionStorage.removeItem(EVENT_LOG_KEY)
}
function addEvent(data) {
const eventLog = getEventLog()
eventLog.push(data)
sessionStorage.setItem(EVENT_LOG_KEY, JSON.stringify(eventLog))
}
if (typeof window !== 'undefined') {
// global functions introduced to interface with the test infrastructure
window._clearEventLog = clearEventLog
window._getEventLog = getEventLog
}
function useLoggedEvent(event, serializeArgs = (...args) => args) {
const router = useRouter()
useEffect(() => {
const logEvent = (...args) => {
addEvent([event, ...serializeArgs(...args)])
}
router.events.on(event, logEvent)
return () => router.events.off(event, logEvent)
}, [event, router.events, serializeArgs])
}
function serializeErrorEventArgs(err, url, properties) {
return [err.message, err.cancelled, url, properties]
}
export default function MyApp({ Component, pageProps }) {
useLoggedEvent('routeChangeStart')
useLoggedEvent('routeChangeComplete')
useLoggedEvent('routeChangeError', serializeErrorEventArgs)
useLoggedEvent('beforeHistoryChange')
useLoggedEvent('hashChangeStart')
useLoggedEvent('hashChangeComplete')
return <Component {...pageProps} />
}

View File

@@ -0,0 +1,25 @@
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
export async function getServerSideProps({ query: { port } }) {
if (!port) {
throw new Error('port required')
}
return { props: { port } }
}
export default function Page({ port }) {
const router = useRouter()
return (
<>
<Link
href={`http://localhost:${port}${router.basePath}/something-else`}
id="absolute-link"
>
http://localhost:{port}
{router.basePath}/something-else
</Link>
</>
)
}

View File

@@ -0,0 +1,22 @@
import React from 'react'
import Link from 'next/link'
export async function getServerSideProps({ query: { port } }) {
if (!port) {
throw new Error('port required')
}
return { props: { port } }
}
export default function Page({ port }) {
return (
<>
<Link
href={`http://localhost:${port}/rewrite-no-basepath`}
id="absolute-link"
>
http://localhost:{port}/rewrite-no-basepath
</Link>
</>
)
}

View File

@@ -0,0 +1,16 @@
import React from 'react'
import Link from 'next/link'
export default function Page() {
return (
<>
<Link href="https://vercel.com/" id="absolute-link">
https://vercel.com/
</Link>
<br />
<Link href="mailto:idk@idk.com" id="mailto-link">
mailto:idk@idk.com
</Link>
</>
)
}

View File

@@ -0,0 +1,4 @@
import { useRouter } from 'next/router'
const Page = () => <p>parts: {useRouter().query.parts?.join('/')}</p>
export default Page

View File

@@ -0,0 +1,2 @@
const Page = () => <p>hello from another</p>
export default Page

View File

@@ -0,0 +1,8 @@
export async function getServerSideProps() {
// We will use this route to simulate a route change errors
throw new Error('KABOOM!')
}
export default function Page() {
return null
}

View File

@@ -0,0 +1,12 @@
const Page = ({ from }) => (
<div>
<p>{from}</p>
<a href="https://google.com">External link</a>
</div>
)
Page.getInitialProps = () => {
return { from: typeof window === 'undefined' ? 'server' : 'client' }
}
export default Page

View File

@@ -0,0 +1,18 @@
import { useRouter } from 'next/router'
export const getStaticProps = () => {
return {
props: {
hello: 'world',
random: Math.random(),
},
}
}
export default (props) => (
<>
<h3 id="gsp">getStaticProps</h3>
<p id="props">{JSON.stringify(props)}</p>
<div id="pathname">{useRouter().pathname}</div>
</>
)

View File

@@ -0,0 +1,19 @@
import { useRouter } from 'next/router'
export const getServerSideProps = () => {
return {
props: {
hello: 'world',
random: Math.random(),
},
}
}
export default (props) => (
<>
<h3 id="gssp">getServerSideProps</h3>
<p id="props">{JSON.stringify(props)}</p>
<div id="pathname">{useRouter().pathname}</div>
<div id="asPath">{useRouter().asPath}</div>
</>
)

View File

@@ -0,0 +1,75 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
function Page() {
const router = useRouter()
const routerObj = router.isReady ? router : { pathname: '', asPath: '' }
return (
<>
<Link href="/other-page" id="other-page-link">
<h1>Hello World</h1>
</Link>
<br />
<Link href="/gsp" id="gsp-link">
<h1>getStaticProps</h1>
</Link>
<br />
<Link href="/gssp" id="gssp-link">
<h1>getServerSideProps</h1>
</Link>
<br />
<Link href="/[slug]" as="/first" id="dynamic-link">
<h1>dynamic page</h1>
</Link>
<br />
<Link
href="/catchall/[...parts]"
as="/catchall/hello/world"
id="catchall-link"
>
<h1>catchall page</h1>
</Link>
<br />
<Link href="/" id="index-gsp">
<h1>index getStaticProps</h1>
</Link>
<br />
<Link href="/index" id="nested-index-gsp">
<h1>nested index getStaticProps</h1>
</Link>
<Link href="#hashlink" id="hashlink">
Hash Link
</Link>
<br />
<div id="base-path">{router.basePath}</div>
<div id="pathname" suppressHydrationWarning>
{routerObj.pathname}
</div>
<div
id="trigger-error"
onClick={() => {
throw new Error('oops heres an error')
}}
>
click me for error
</div>
<br />
<div id="as-path" suppressHydrationWarning>
{routerObj.asPath}
</div>
<Link href="/slow-route" id="slow-route">
<h1>Slow route</h1>
</Link>
<Link href="/error-route" id="error-route">
<h1>Error route</h1>
</Link>
<Link href="/hello#some-hash" id="hash-change">
<h1>Hash change</h1>
</Link>
<Link href="/something-else" as="/hello" id="something-else-link">
to something else
</Link>
</>
)
}
export default Page

View File

@@ -0,0 +1,35 @@
import { useRouter } from 'next/router'
import Link from 'next/link'
import { useState } from 'react'
import { useEffect } from 'react'
export const getStaticProps = () => {
return {
props: {
nested: false,
hello: 'hello',
},
}
}
export default function Index({ hello, nested }) {
const { query, pathname, asPath } = useRouter()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
return () => setMounted(false)
}, [])
return (
<>
<h1 id="index-page">index page</h1>
<p id="nested">{nested ? 'yes' : 'no'}</p>
<p id="prop">{hello} world</p>
<p id="query">{JSON.stringify(query)}</p>
<p id="pathname">{pathname}</p>
<p id="as-path">{mounted ? asPath : ''}</p>
<Link href="/hello" id="hello-link">
to /hello
</Link>
</>
)
}

View File

@@ -0,0 +1,22 @@
import { useRouter } from 'next/router'
export const getStaticProps = () => {
return {
props: {
nested: true,
hello: 'hello',
},
}
}
export default function Index({ hello, nested }) {
const { query, pathname } = useRouter()
return (
<>
<p id="nested">{nested ? 'yes' : 'no'}</p>
<p id="prop">{hello} world</p>
<p id="query">{JSON.stringify(query)}</p>
<p id="pathname">{pathname}</p>
</>
)
}

View File

@@ -0,0 +1,12 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
const Page = () => (
<>
<Link href={`${useRouter().basePath}/other-page`} id="other-page-link">
<h1>Hello World</h1>
</Link>
</>
)
export default Page

View File

@@ -0,0 +1,11 @@
import Link from 'next/link'
export default function Page() {
return (
<>
<Link href="/" id="link-back">
back
</Link>
</>
)
}

View File

@@ -0,0 +1,2 @@
const Page = () => <h1 id="other-page-title">Hello Other</h1>
export default Page

View File

@@ -0,0 +1,10 @@
export async function getServerSideProps() {
// We will use this route to simulate a route cancellation error
// by clicking its link twice in rapid succession
await new Promise((resolve) => setTimeout(resolve, 5000))
return { props: {} }
}
export default function Page() {
return null
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="something-else-page">something else</h1>
}

View File

@@ -0,0 +1,11 @@
function SSRPage({ test }) {
return <h1>{test}</h1>
}
SSRPage.getInitialProps = () => {
return {
test: 'hello',
}
}
export default SSRPage

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1,59 @@
import webdriver from 'next-webdriver'
import { check } from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
describe('basePath query/hash handling', () => {
const basePath = '/docs'
const { next } = nextTestSetup({
files: __dirname,
nextConfig: {
basePath,
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},
},
})
it.each([
{ hash: '#hello?' },
{ hash: '#?' },
{ hash: '##' },
{ hash: '##?' },
{ hash: '##hello?' },
{ hash: '##hello' },
{ hash: '#hello?world' },
{ search: '?hello=world', hash: '#a', query: { hello: 'world' } },
{ search: '?hello', hash: '#a', query: { hello: '' } },
{ search: '?hello=', hash: '#a', query: { hello: '' } },
])(
'is correct during query updating $hash $search',
async ({ hash, search, query }) => {
const browser = await webdriver(
next.url,
`${basePath}${search || ''}${hash || ''}`
)
await check(
() =>
browser.eval('window.next.router.isReady ? "ready" : "not ready"'),
'ready'
)
expect(await browser.eval('window.location.pathname')).toBe(basePath)
expect(await browser.eval('window.location.search')).toBe(search || '')
expect(await browser.eval('window.location.hash')).toBe(hash || '')
expect(await browser.eval('next.router.pathname')).toBe('/')
expect(
JSON.parse(await browser.eval('JSON.stringify(next.router.query)'))
).toEqual(query || {})
}
)
it('should work with hash links', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
await browser.elementByCss('#hashlink').click()
const url = new URL(await browser.eval(() => window.location.href))
expect(url.pathname).toBe(`${basePath}/hello`)
expect(url.hash).toBe('#hashlink')
})
})

View File

@@ -0,0 +1,154 @@
import { nextTestSetup } from 'e2e-utils'
import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
describe('basePath', () => {
const basePath = '/docs'
const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
nextConfig: {
basePath,
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},
async rewrites() {
return [
{
source: '/rewrite-1',
destination: '/gssp',
},
{
source: '/rewrite-no-basepath',
destination: 'https://example.vercel.sh',
basePath: false,
},
{
source: '/rewrite/chain-1',
destination: '/rewrite/chain-2',
},
{
source: '/rewrite/chain-2',
destination: '/gssp',
},
]
},
async redirects() {
return [
{
source: '/redirect-1',
destination: '/somewhere-else',
permanent: false,
},
{
source: '/redirect-no-basepath',
destination: '/another-destination',
permanent: false,
basePath: false,
},
]
},
async headers() {
return [
{
source: '/add-header',
headers: [
{
key: 'x-hello',
value: 'world',
},
],
},
{
source: '/add-header-no-basepath',
basePath: false,
headers: [
{
key: 'x-hello',
value: 'world',
},
],
},
]
},
},
})
it('should rewrite with basePath by default', async () => {
const html = await renderViaHTTP(next.url, `${basePath}/rewrite-1`)
expect(html).toContain('getServerSideProps')
})
it('should not rewrite without basePath without disabling', async () => {
const res = await fetchViaHTTP(next.url, '/rewrite-1')
expect(res.status).toBe(404)
})
it('should not rewrite with basePath when set to false', async () => {
// won't 404 as it matches the dynamic [slug] route
const html = await renderViaHTTP(
next.url,
`${basePath}/rewrite-no-basePath`
)
expect(html).toContain('slug')
})
it('should rewrite without basePath when set to false', async () => {
const html = await renderViaHTTP(next.url, '/rewrite-no-basePath')
expect(html).toContain('Example Domain')
})
it('should redirect with basePath by default', async () => {
const res = await fetchViaHTTP(
next.url,
`${basePath}/redirect-1`,
undefined,
{
redirect: 'manual',
}
)
const { pathname } = new URL(res.headers.get('location') || '')
expect(pathname).toBe(`${basePath}/somewhere-else`)
expect(res.status).toBe(307)
const text = await res.text()
if (!isNextDeploy) {
expect(text).toContain(`${basePath}/somewhere-else`)
}
})
it('should not redirect without basePath without disabling', async () => {
const res = await fetchViaHTTP(next.url, '/redirect-1', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(404)
})
it('should not redirect with basePath when set to false', async () => {
// won't 404 as it matches the dynamic [slug] route
const html = await renderViaHTTP(
next.url,
`${basePath}/rewrite-no-basePath`
)
expect(html).toContain('slug')
})
it('should redirect without basePath when set to false', async () => {
const res = await fetchViaHTTP(
next.url,
'/redirect-no-basepath',
undefined,
{
redirect: 'manual',
}
)
const { pathname } = new URL(res.headers.get('location') || '')
expect(pathname).toBe('/another-destination')
expect(res.status).toBe(307)
const text = await res.text()
if (!isNextDeploy) {
expect(text).toContain('/another-destination')
}
})
})

View File

@@ -0,0 +1,132 @@
import assert from 'assert'
import webdriver from 'next-webdriver'
import { nextTestSetup } from 'e2e-utils'
import { check, retry } from 'next-test-utils'
describe('basePath', () => {
const basePath = '/docs'
const { next } = nextTestSetup({
files: __dirname,
nextConfig: {
basePath,
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},
},
})
it('should use urls with basepath in router events', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
try {
await check(
() => browser.eval('window.next.router.isReady ? "ready" : "no"'),
'ready'
)
await browser.eval('window._clearEventLog()')
await browser
.elementByCss('#other-page-link')
.click()
.waitForElementByCss('#other-page-title')
const eventLog = await browser.eval('window._getEventLog()')
expect(
eventLog.filter((item) => item[1]?.endsWith('/other-page'))
).toEqual([
['routeChangeStart', `${basePath}/other-page`, { shallow: false }],
['beforeHistoryChange', `${basePath}/other-page`, { shallow: false }],
['routeChangeComplete', `${basePath}/other-page`, { shallow: false }],
])
} finally {
await browser.close()
}
})
it('should use urls with basepath in router events for hash changes', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
try {
await check(
() => browser.eval('window.next.router.isReady ? "ready" : "no"'),
'ready'
)
await browser.eval('window._clearEventLog()')
await browser.elementByCss('#hash-change').click()
const eventLog = await browser.eval('window._getEventLog()')
expect(eventLog).toEqual([
['hashChangeStart', `${basePath}/hello#some-hash`, { shallow: false }],
[
'hashChangeComplete',
`${basePath}/hello#some-hash`,
{ shallow: false },
],
])
} finally {
await browser.close()
}
})
it('should use urls with basepath in router events for cancelled routes', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
try {
await check(
() => browser.eval('window.next.router.isReady ? "ready" : "no"'),
'ready'
)
await browser.eval('window._clearEventLog()')
await browser
.elementByCss('#slow-route')
.click()
.elementByCss('#other-page-link')
.click()
.waitForElementByCss('#other-page-title')
const eventLog = await browser.eval('window._getEventLog()')
expect(eventLog).toEqual([
['routeChangeStart', `${basePath}/slow-route`, { shallow: false }],
[
'routeChangeError',
'Route Cancelled',
true,
`${basePath}/slow-route`,
{ shallow: false },
],
['routeChangeStart', `${basePath}/other-page`, { shallow: false }],
['beforeHistoryChange', `${basePath}/other-page`, { shallow: false }],
['routeChangeComplete', `${basePath}/other-page`, { shallow: false }],
])
} finally {
await browser.close()
}
})
it('should use urls with basepath in router events for failed route change', async () => {
const browser = await webdriver(next.url, `${basePath}/hello`)
try {
await check(
() => browser.eval('window.next.router.isReady ? "ready" : "no"'),
'ready'
)
await browser.eval('window._clearEventLog()')
await browser.elementByCss('#error-route').click()
await retry(async () => {
const eventLog = await browser.eval('window._getEventLog()')
assert.deepEqual(eventLog, [
['routeChangeStart', `${basePath}/error-route`, { shallow: false }],
[
'routeChangeError',
'Failed to load static props',
null,
`${basePath}/error-route`,
{ shallow: false },
],
])
}, 10_000)
} finally {
await browser.close()
}
})
})

View File

@@ -0,0 +1,95 @@
import webdriver from 'next-webdriver'
import { createNext } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import { waitForNoRedbox } from 'next-test-utils'
describe('basePath + trailingSlash', () => {
let next: NextInstance
const basePath = '/docs'
beforeAll(async () => {
next = await createNext({
files: __dirname,
nextConfig: {
trailingSlash: true,
basePath,
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},
},
})
})
afterAll(() => next.destroy())
const runTests = (dev = false) => {
it('should allow URL query strings without refresh', async () => {
const browser = await webdriver(next.url, `${basePath}/hello/?query=true`)
try {
await browser.eval('window.itdidnotrefresh = "hello"')
await new Promise((resolve, reject) => {
// Timeout of EventSource created in setupPing()
// (on-demand-entries-utils.js) is 5000 ms (see #13132, #13560)
setTimeout(resolve, dev ? 10000 : 1000)
})
expect(await browser.eval('window.itdidnotrefresh')).toBe('hello')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/hello')
expect(await browser.eval('window.location.pathname')).toBe(
`${basePath}/hello/`
)
expect(await browser.eval('window.location.search')).toBe('?query=true')
if (dev) {
await waitForNoRedbox(browser)
}
} finally {
await browser.close()
}
})
it('should allow URL query strings on index without refresh', async () => {
const browser = await webdriver(next.url, `${basePath}/?query=true`)
try {
await browser.eval('window.itdidnotrefresh = "hello"')
await new Promise((resolve, reject) => {
// Timeout of EventSource created in setupPing()
// (on-demand-entries-utils.js) is 5000 ms (see #13132, #13560)
setTimeout(resolve, dev ? 10000 : 1000)
})
expect(await browser.eval('window.itdidnotrefresh')).toBe('hello')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/')
expect(await browser.eval('window.location.pathname')).toBe(
basePath + '/'
)
expect(await browser.eval('window.location.search')).toBe('?query=true')
if (dev) {
await waitForNoRedbox(browser)
}
} finally {
await browser.close()
}
})
it('should correctly replace state when same asPath but different url', async () => {
const browser = await webdriver(next.url, `${basePath}/`)
try {
await browser.elementByCss('#hello-link').click()
await browser.waitForElementByCss('#something-else-link')
await browser.elementByCss('#something-else-link').click()
await browser.waitForElementByCss('#something-else-page')
await browser.back()
await browser.waitForElementByCss('#index-page')
await browser.forward()
await browser.waitForElementByCss('#something-else-page')
} finally {
await browser.close()
}
})
}
runTests((global as any).isDev)
})