mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What? This PR fixes interception routes to work correctly with the `trailingSlash` configuration option. ### Why? Interception routes were not working when users had `trailingSlash: true` in their Next.js config. This was because interception route rewrites were marked as `internal: true`, which caused them to bypass the `modifyRouteRegex` processing that handles trailing slash normalization. When a route is marked as internal and provides its own regex, the code would skip the regex modification step that ensures routes work correctly with both `trailingSlash: true` and `trailingSlash: false` configurations. This meant interception routes would fail to match when trailing slashes were enabled. ### How? 1. **Removed `internal: true` flag** from interception route rewrites in `generate-interception-routes-rewrites.ts`. This ensures they go through the same regex modification process as regular routes, properly handling trailing slash normalization. 2. **Added comprehensive test coverage** by wrapping the parallel routes and interception test suite in `describe.each([true, false])` to test both trailing slash configurations. Updated all URL assertions to account for the trailing slash setting. 3. **Refactored `build-custom-route.ts`** to scope the `source` variable closer to where it's actually used, preventing unnecessary computation for internal routes that already provide a regex. 4. **Moved `next.config.js` inline** to the test file for better organization and to support the parameterized test structure.
1092 lines
34 KiB
TypeScript
1092 lines
34 KiB
TypeScript
import { nextTestSetup, FileRef } from 'e2e-utils'
|
|
import { NextConfig } from 'next'
|
|
import { check, retry } from 'next-test-utils'
|
|
import path from 'path'
|
|
|
|
const nextConfig: NextConfig = {
|
|
async rewrites() {
|
|
return {
|
|
beforeFiles: [
|
|
{
|
|
source: '/foo',
|
|
destination: '/en/foo',
|
|
},
|
|
{
|
|
source: '/photos',
|
|
destination: '/en/photos',
|
|
},
|
|
],
|
|
}
|
|
},
|
|
}
|
|
|
|
describe.each([true, false])(
|
|
'parallel-routes-and-interception (trailingSlash: %s)',
|
|
(trailingSlash) => {
|
|
const { next, isNextDev, isNextStart } = nextTestSetup({
|
|
files: __dirname,
|
|
nextConfig: {
|
|
trailingSlash,
|
|
...nextConfig,
|
|
},
|
|
})
|
|
|
|
describe('parallel routes', () => {
|
|
it('should support parallel route tab bars', async () => {
|
|
const browser = await next.browser('/parallel-tab-bar')
|
|
|
|
const hasHome = async () => {
|
|
await check(
|
|
() => browser.waitForElementByCss('#home').text(),
|
|
'Tab bar page (@children)'
|
|
)
|
|
}
|
|
const hasViewsHome = async () => {
|
|
await check(
|
|
() => browser.waitForElementByCss('#views-home').text(),
|
|
'Views home'
|
|
)
|
|
}
|
|
const hasViewDuration = async () => {
|
|
await check(
|
|
() => browser.waitForElementByCss('#view-duration').text(),
|
|
'View duration'
|
|
)
|
|
}
|
|
const hasImpressions = async () => {
|
|
await check(
|
|
() => browser.waitForElementByCss('#impressions').text(),
|
|
'Impressions'
|
|
)
|
|
}
|
|
const hasAudienceHome = async () => {
|
|
await check(
|
|
() => browser.waitForElementByCss('#audience-home').text(),
|
|
'Audience home'
|
|
)
|
|
}
|
|
const hasDemographics = async () => {
|
|
await check(
|
|
() => browser.waitForElementByCss('#demographics').text(),
|
|
'Demographics'
|
|
)
|
|
}
|
|
const hasSubscribers = async () => {
|
|
await check(
|
|
() => browser.waitForElementByCss('#subscribers').text(),
|
|
'Subscribers'
|
|
)
|
|
}
|
|
const checkUrlPath = async (path: string) => {
|
|
await check(
|
|
() => browser.url(),
|
|
`${next.url}/parallel-tab-bar${path}${trailingSlash ? '/' : ''}`
|
|
)
|
|
}
|
|
|
|
// Initial page
|
|
const step1 = async () => {
|
|
await hasHome()
|
|
await hasViewsHome()
|
|
await hasAudienceHome()
|
|
await checkUrlPath('')
|
|
}
|
|
|
|
await step1()
|
|
|
|
console.log('step1')
|
|
// Navigate to /views/duration
|
|
await browser.elementByCss('#view-duration-link').click()
|
|
|
|
const step2 = async () => {
|
|
await hasHome()
|
|
await hasViewDuration()
|
|
await hasAudienceHome()
|
|
await checkUrlPath('/view-duration')
|
|
}
|
|
|
|
await step2()
|
|
console.log('step2')
|
|
|
|
// Navigate to /views/impressions
|
|
await browser.elementByCss('#impressions-link').click()
|
|
|
|
const step3 = async () => {
|
|
await hasHome()
|
|
await hasImpressions()
|
|
await hasAudienceHome()
|
|
await checkUrlPath('/impressions')
|
|
}
|
|
|
|
await step3()
|
|
console.log('step3')
|
|
|
|
// Navigate to /audience/demographics
|
|
await browser.elementByCss('#demographics-link').click()
|
|
|
|
const step4 = async () => {
|
|
await hasHome()
|
|
await hasImpressions()
|
|
await hasDemographics()
|
|
await checkUrlPath('/demographics')
|
|
}
|
|
|
|
await step4()
|
|
console.log('step4')
|
|
|
|
// Navigate to /audience/subscribers
|
|
await browser.elementByCss('#subscribers-link').click()
|
|
|
|
const step5 = async () => {
|
|
await hasHome()
|
|
await hasImpressions()
|
|
await hasSubscribers()
|
|
await checkUrlPath('/subscribers')
|
|
}
|
|
|
|
await step5()
|
|
console.log('step5')
|
|
|
|
// Navigate to /
|
|
await browser.elementByCss('#home-link-audience').click()
|
|
|
|
await checkUrlPath('')
|
|
|
|
// TODO: home link behavior
|
|
// await step1()
|
|
|
|
// TODO: fix back/forward navigation test
|
|
// Test that back navigation works as intended
|
|
await browser.back()
|
|
await step5()
|
|
console.log('step5 back')
|
|
await browser.back()
|
|
await step4()
|
|
console.log('step4 back')
|
|
await browser.back()
|
|
await step3()
|
|
console.log('step3 back')
|
|
|
|
await browser.back()
|
|
await step2()
|
|
console.log('step2 back')
|
|
await browser.back()
|
|
await step1()
|
|
console.log('step1 back')
|
|
console.log('step6')
|
|
|
|
// Test that forward navigation works as intended
|
|
await browser.forward()
|
|
await step2()
|
|
console.log('step2 forward')
|
|
await browser.forward()
|
|
await step3()
|
|
console.log('step3 forward')
|
|
await browser.forward()
|
|
await step4()
|
|
console.log('step4 forward')
|
|
await browser.forward()
|
|
await step5()
|
|
})
|
|
|
|
it('should match parallel routes', async () => {
|
|
const $ = await next.render$('/parallel/nested')
|
|
const pageText = $('#parallel-layout').text()
|
|
expect(pageText).toContain('parallel/layout')
|
|
expect(pageText).toContain('parallel/@foo/nested/layout')
|
|
expect(pageText).toContain('parallel/@foo/nested/@a/page')
|
|
expect(pageText).toContain('parallel/@foo/nested/@b/page')
|
|
expect(pageText).toContain('parallel/@bar/nested/layout')
|
|
expect(pageText).toContain('parallel/@bar/nested/@a/page')
|
|
expect(pageText).toContain('parallel/@bar/nested/@b/page')
|
|
expect(pageText).toContain('parallel/nested/page')
|
|
})
|
|
|
|
it('should match parallel routes in route groups', async () => {
|
|
const $ = await next.render$('/parallel/nested-2')
|
|
const pageText = $('#parallel-layout').text()
|
|
expect(pageText).toContain('parallel/layout')
|
|
expect(pageText).toContain('parallel/(new)/layout')
|
|
expect(pageText).toContain('parallel/(new)/@baz/nested/page')
|
|
})
|
|
|
|
it('should throw a 404 when no matching parallel route is found', async () => {
|
|
const browser = await next.browser('/parallel-tab-bar')
|
|
// we make sure the page is available through navigating
|
|
await check(
|
|
() => browser.waitForElementByCss('#home').text(),
|
|
'Tab bar page (@children)'
|
|
)
|
|
await browser.elementByCss('#view-duration-link').click()
|
|
await check(
|
|
() => browser.waitForElementByCss('#view-duration').text(),
|
|
'View duration'
|
|
)
|
|
|
|
// fetch /parallel-tab-bar/view-duration
|
|
const res = await next.fetch(
|
|
`${next.url}/parallel-tab-bar/view-duration`
|
|
)
|
|
const html = await res.text()
|
|
expect(html).toContain('page could not be found')
|
|
})
|
|
|
|
it('should render nested parallel routes', async () => {
|
|
const browser = await next.browser('/parallel-side-bar/nested/deeper')
|
|
await check(
|
|
() => browser.waitForElementByCss('#nested-deeper-main').text(),
|
|
'Nested deeper page'
|
|
)
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#nested-deeper-sidebar').text(),
|
|
'Nested deeper sidebar here'
|
|
)
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-side-bar/nested${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#nested-main').text(),
|
|
'Nested page'
|
|
)
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#nested-sidebar').text(),
|
|
'Nested sidebar here'
|
|
)
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-side-bar${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#main').text(),
|
|
'homepage'
|
|
)
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#sidebar-main').text(),
|
|
'root sidebar here'
|
|
)
|
|
})
|
|
|
|
it('should support layout files in parallel routes', async () => {
|
|
const browser = await next.browser('/parallel-layout')
|
|
await check(
|
|
() => browser.waitForElementByCss('#parallel-layout').text(),
|
|
'parallel layout'
|
|
)
|
|
|
|
// navigate to /parallel-layout/subroute
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-layout/subroute${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await check(
|
|
() => browser.waitForElementByCss('#parallel-layout').text(),
|
|
'parallel layout'
|
|
)
|
|
await check(
|
|
() => browser.waitForElementByCss('#parallel-subroute').text(),
|
|
'parallel subroute layout'
|
|
)
|
|
})
|
|
|
|
it('should only scroll to the parallel route that was navigated to', async () => {
|
|
const browser = await next.browser('/parallel-scroll')
|
|
|
|
await browser.eval('window.scrollTo(0, 1000)')
|
|
const position = await browser.eval('window.scrollY')
|
|
console.log('position', position)
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-scroll/nav${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await browser.waitForElementByCss('#modal')
|
|
// check that we didn't scroll back to the top
|
|
await check(() => browser.eval('window.scrollY'), position)
|
|
})
|
|
|
|
it('should apply the catch-all route to the parallel route if no matching route is found', async () => {
|
|
const browser = await next.browser('/parallel-catchall')
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-catchall/bar${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await check(
|
|
() => browser.waitForElementByCss('#main').text(),
|
|
'bar slot'
|
|
)
|
|
await check(
|
|
() => browser.waitForElementByCss('#slot-content').text(),
|
|
'slot catchall'
|
|
)
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-catchall/foo${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await check(() => browser.waitForElementByCss('#main').text(), 'foo')
|
|
await check(
|
|
() => browser.waitForElementByCss('#slot-content').text(),
|
|
'foo slot'
|
|
)
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-catchall/baz${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await check(
|
|
() => browser.waitForElementByCss('#main').text(),
|
|
/main catchall/
|
|
)
|
|
await check(
|
|
() => browser.waitForElementByCss('#main').text(),
|
|
/catchall page client component/
|
|
)
|
|
await check(
|
|
() => browser.waitForElementByCss('#slot-content').text(),
|
|
'baz slot'
|
|
)
|
|
})
|
|
|
|
it('should match the catch-all routes of the more specific path, if there is more than one catch-all route', async () => {
|
|
const browser = await next.browser('/parallel-nested-catchall')
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-nested-catchall/foo${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await check(() => browser.waitForElementByCss('#main').text(), 'foo')
|
|
await check(
|
|
() => browser.waitForElementByCss('#slot-content').text(),
|
|
'foo slot'
|
|
)
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-nested-catchall/bar${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await check(() => browser.waitForElementByCss('#main').text(), 'bar')
|
|
await check(
|
|
() => browser.waitForElementByCss('#slot-content').text(),
|
|
'slot catchall'
|
|
)
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/parallel-nested-catchall/foo/123${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
await check(() => browser.waitForElementByCss('#main').text(), 'foo id')
|
|
await check(
|
|
() => browser.waitForElementByCss('#slot-content').text(),
|
|
'foo id catchAll'
|
|
)
|
|
})
|
|
|
|
it('should navigate with a link with prefetch=false', async () => {
|
|
const browser = await next.browser('/parallel-prefetch-false')
|
|
|
|
// check if the default view loads
|
|
await check(
|
|
() => browser.waitForElementByCss('#default-parallel').text(),
|
|
'default view for parallel'
|
|
)
|
|
|
|
// check that navigating to /foo re-renders the layout to display @parallel/foo
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/parallel-prefetch-false/foo${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#parallel-foo')
|
|
.text(),
|
|
'parallel for foo'
|
|
)
|
|
})
|
|
|
|
it('should display all parallel route params with useParams', async () => {
|
|
const browser = await next.browser('/parallel-dynamic/foo/bar')
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#foo').text(),
|
|
`{"slug":"foo","id":"bar"}`
|
|
)
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#bar').text(),
|
|
`{"slug":"foo","id":"bar"}`
|
|
)
|
|
})
|
|
|
|
it('should load CSS for a default page that exports another page', async () => {
|
|
const browser = await next.browser('/default-css')
|
|
|
|
expect(
|
|
await browser.eval(
|
|
`window.getComputedStyle(document.getElementById("red-text")).color`
|
|
)
|
|
).toBe('rgb(255, 0, 0)')
|
|
|
|
// the more page will now be using the page's `default.tsx` file, which re-exports the root page.
|
|
await browser
|
|
.elementByCss(`[href="/default-css/more${trailingSlash ? '/' : ''}"]`)
|
|
.click()
|
|
|
|
expect(
|
|
await browser.eval(
|
|
`window.getComputedStyle(document.getElementById("red-text")).color`
|
|
)
|
|
).toBe('rgb(255, 0, 0)')
|
|
|
|
// ensure that everything still works on a fresh load
|
|
await browser.refresh()
|
|
|
|
expect(
|
|
await browser.eval(
|
|
`window.getComputedStyle(document.getElementById("red-text")).color`
|
|
)
|
|
).toBe('rgb(255, 0, 0)')
|
|
})
|
|
|
|
it('should handle a loading state', async () => {
|
|
const browser = await next.browser('/with-loading')
|
|
expect(await browser.elementById('slot').text()).toBe('Root Slot')
|
|
expect(await browser.elementById('children').text()).toBe('Root Page')
|
|
|
|
// should have triggered a loading state
|
|
expect(
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/with-loading/foo${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#loading-page')
|
|
.text()
|
|
).toBe('Loading...')
|
|
|
|
// should eventually load the full page
|
|
await retry(async () => {
|
|
expect(await browser.elementById('slot').text()).toBe('Nested Slot')
|
|
expect(await browser.elementById('children').text()).toBe(
|
|
'Welcome to Foo Page'
|
|
)
|
|
})
|
|
})
|
|
|
|
if (isNextDev) {
|
|
it('should support parallel routes with no page component', async () => {
|
|
const browser = await next.browser('/parallel-no-page/foo')
|
|
const timestamp = await browser.elementByCss('#timestamp').text()
|
|
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, 3000)
|
|
})
|
|
|
|
await check(async () => {
|
|
// an invalid response triggers a fast refresh, so if the timestamp doesn't update, this behaved correctly
|
|
const newTimestamp = await browser.elementByCss('#timestamp').text()
|
|
return newTimestamp !== timestamp ? 'failure' : 'success'
|
|
}, 'success')
|
|
})
|
|
|
|
it('should support nested parallel routes', async () => {
|
|
const browser = await next.browser('parallel-nested/home/nested')
|
|
const timestamp = await browser.elementByCss('#timestamp').text()
|
|
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, 3000)
|
|
})
|
|
|
|
await check(async () => {
|
|
// an invalid response triggers a fast refresh, so if the timestamp doesn't update, this behaved correctly
|
|
const newTimestamp = await browser.elementByCss('#timestamp').text()
|
|
return newTimestamp !== timestamp ? 'failure' : 'success'
|
|
}, 'success')
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('route intercepting with dynamic routes', () => {
|
|
it('should render intercepted route', async () => {
|
|
const browser = await next.browser(
|
|
`/intercepting-routes-dynamic/photos${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
// Check if navigation to modal route works
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-routes-dynamic/photos/next/123${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#user-intercept-page')
|
|
.text(),
|
|
'Intercepted Page'
|
|
)
|
|
|
|
// Check if url matches even though it was intercepted.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes-dynamic/photos/next/123' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
|
|
// Trigger a refresh, this should load the normal page, not the modal.
|
|
await check(
|
|
() =>
|
|
browser.refresh().waitForElementByCss('#user-regular-page').text(),
|
|
'Regular Page'
|
|
)
|
|
|
|
// Check if the url matches still.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes-dynamic/photos/next/123' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('route intercepting with prerendered dynamic routes ', () => {
|
|
it('should render intercepted route', async () => {
|
|
const browser = await next.browser(
|
|
'/intercepting-routes-dynamic-prerendered/photos'
|
|
)
|
|
|
|
// Check if navigation to modal route works.
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/intercepting-routes-dynamic-prerendered/photos/1${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
|
|
// This should load the intercepted page.
|
|
await retry(async () => {
|
|
expect(
|
|
await browser.waitForElementByCss('#photo-intercepted-1').text()
|
|
).toBe('Photo INTERCEPTED 1')
|
|
})
|
|
|
|
// Check if url matches even though it was intercepted.
|
|
expect(await browser.url()).toBe(
|
|
next.url +
|
|
'/intercepting-routes-dynamic-prerendered/photos/1' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
|
|
// There must not be any errors from prefetching the intercepted page.
|
|
expect(
|
|
(await browser.log()).filter(({ source }) => source === 'error')
|
|
).toEqual([])
|
|
|
|
// Trigger a refresh, this should load the normal page, not the modal.
|
|
await browser.refresh()
|
|
expect(await browser.waitForElementByCss('#photo-page-1').text()).toBe(
|
|
'Photo PAGE 1'
|
|
)
|
|
|
|
// Check if the url matches still.
|
|
expect(await browser.url()).toBe(
|
|
next.url +
|
|
'/intercepting-routes-dynamic-prerendered/photos/1' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('route intercepting with dynamic optional catch-all routes', () => {
|
|
it('should render intercepted route', async () => {
|
|
const browser = await next.browser(
|
|
`/intercepting-routes-dynamic-catchall/photos${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
// Check if navigation to modal route works
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-routes-dynamic-catchall/photos/optional-catchall/123${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#optional-catchall-intercept-page')
|
|
.text(),
|
|
'Intercepted Page'
|
|
)
|
|
|
|
// Check if url matches even though it was intercepted.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes-dynamic-catchall/photos/optional-catchall/123' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
|
|
// Trigger a refresh, this should load the normal page, not the modal.
|
|
await check(
|
|
() =>
|
|
browser
|
|
.refresh()
|
|
.waitForElementByCss('#optional-catchall-regular-page')
|
|
.text(),
|
|
'Regular Page'
|
|
)
|
|
|
|
// Check if the url matches still.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes-dynamic-catchall/photos/optional-catchall/123' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('route intercepting with dynamic catch-all routes', () => {
|
|
it('should render intercepted route', async () => {
|
|
const browser = await next.browser(
|
|
`/intercepting-routes-dynamic-catchall/photos${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
// Check if navigation to modal route works
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-routes-dynamic-catchall/photos/catchall/123${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#catchall-intercept-page')
|
|
.text(),
|
|
'Intercepted Page'
|
|
)
|
|
|
|
// Check if url matches even though it was intercepted.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes-dynamic-catchall/photos/catchall/123' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
|
|
// Trigger a refresh, this should load the normal page, not the modal.
|
|
await check(
|
|
() =>
|
|
browser
|
|
.refresh()
|
|
.waitForElementByCss('#catchall-regular-page')
|
|
.text(),
|
|
'Regular Page'
|
|
)
|
|
|
|
// Check if the url matches still.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes-dynamic-catchall/photos/catchall/123' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('route intercepting', () => {
|
|
it('should render intercepted route', async () => {
|
|
const browser = await next.browser(
|
|
`/intercepting-routes/feed${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
// Check if navigation to modal route works.
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-routes/feed/photos/1${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#photo-intercepted-1')
|
|
.text(),
|
|
'Photo INTERCEPTED 1'
|
|
)
|
|
|
|
// Check if intercepted route was rendered while existing page content was removed.
|
|
// Content would only be preserved when combined with parallel routes.
|
|
// await check(() => browser.elementByCss('#feed-page').text()).not.toBe('Feed')
|
|
|
|
// Check if url matches even though it was intercepted.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes/feed/photos/1' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
|
|
// Trigger a refresh, this should load the normal page, not the modal.
|
|
await check(
|
|
() => browser.refresh().waitForElementByCss('#photo-page-1').text(),
|
|
'Photo PAGE 1'
|
|
)
|
|
|
|
// Check if the url matches still.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes/feed/photos/1' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
})
|
|
|
|
it('should render an intercepted route from a slot', async () => {
|
|
const browser = await next.browser('/')
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#default-slot').text(),
|
|
'default from @slot'
|
|
)
|
|
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(`[href="/nested${trailingSlash ? '/' : ''}"]`)
|
|
.click()
|
|
.waitForElementByCss('#interception-slot')
|
|
.text(),
|
|
'interception from @slot/nested'
|
|
)
|
|
|
|
// Check if the client component is rendered
|
|
await check(
|
|
() => browser.waitForElementByCss('#interception-slot-client').text(),
|
|
'client component'
|
|
)
|
|
|
|
await check(
|
|
() => browser.refresh().waitForElementByCss('#nested').text(),
|
|
'hello world from /nested'
|
|
)
|
|
})
|
|
|
|
it('should render an intercepted route at the top level from a nested path', async () => {
|
|
const browser = await next.browser(
|
|
`/nested-link${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
await check(
|
|
() => browser.waitForElementByCss('#default-slot').text(),
|
|
'default from @slot'
|
|
)
|
|
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(`[href="/nested${trailingSlash ? '/' : ''}"]`)
|
|
.click()
|
|
.waitForElementByCss('#interception-slot')
|
|
.text(),
|
|
'interception from @slot/nested'
|
|
)
|
|
|
|
await check(
|
|
() => browser.refresh().waitForElementByCss('#nested').text(),
|
|
'hello world from /nested'
|
|
)
|
|
})
|
|
|
|
it('should render intercepted route from a nested route', async () => {
|
|
const browser = await next.browser(
|
|
`/intercepting-routes/feed/nested${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
// Check if navigation to modal route works.
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-routes/feed/photos/1${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#photo-intercepted-1')
|
|
.text(),
|
|
'Photo INTERCEPTED 1'
|
|
)
|
|
|
|
// Check if intercepted route was rendered while existing page content was removed.
|
|
// Content would only be preserved when combined with parallel routes.
|
|
// await check(() => browser.elementByCss('#feed-page').text()).not.toBe('Feed')
|
|
|
|
// Check if url matches even though it was intercepted.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes/feed/photos/1' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
|
|
// Trigger a refresh, this should load the normal page, not the modal.
|
|
await check(
|
|
() => browser.refresh().waitForElementByCss('#photo-page-1').text(),
|
|
'Photo PAGE 1'
|
|
)
|
|
|
|
// Check if the url matches still.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-routes/feed/photos/1' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
})
|
|
|
|
it('should re-render the layout on the server when it had a default child route', async () => {
|
|
const browser = await next.browser(
|
|
`/parallel-non-intercepting${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
// check if the default view loads
|
|
await check(
|
|
() => browser.waitForElementByCss('#default-parallel').text(),
|
|
'default view for parallel'
|
|
)
|
|
|
|
// check that navigating to /foo re-renders the layout to display @parallel/foo
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/parallel-non-intercepting/foo${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#parallel-foo')
|
|
.text(),
|
|
'parallel for foo'
|
|
)
|
|
|
|
// check that navigating to /foo also re-renders the base children
|
|
await check(() => browser.elementByCss('#children-foo').text(), 'foo')
|
|
})
|
|
|
|
it('should render modal when paired with parallel routes', async () => {
|
|
const browser = await next.browser(
|
|
`/intercepting-parallel-modal/vercel${trailingSlash ? '/' : ''}`
|
|
)
|
|
// Check if navigation to modal route works.
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-parallel-modal/photo/1${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#photo-modal-1')
|
|
.text(),
|
|
'Photo MODAL 1'
|
|
)
|
|
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-parallel-modal/photo/2${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#photo-modal-2')
|
|
.text(),
|
|
'Photo MODAL 2'
|
|
)
|
|
|
|
// Check if modal was rendered while existing page content is preserved.
|
|
await check(
|
|
() => browser.elementByCss('#user-page').text(),
|
|
'Feed for vercel'
|
|
)
|
|
|
|
// Check if url matches even though it was intercepted.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-parallel-modal/photo/2' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
|
|
// Trigger a refresh, this should load the normal page, not the modal.
|
|
await check(
|
|
() => browser.refresh().waitForElementByCss('#photo-page-2').text(),
|
|
'Photo PAGE 2'
|
|
)
|
|
|
|
// Check if the url matches still.
|
|
await check(
|
|
() => browser.url(),
|
|
next.url +
|
|
'/intercepting-parallel-modal/photo/2' +
|
|
(trailingSlash ? '/' : '')
|
|
)
|
|
})
|
|
|
|
it('should support intercepting with beforeFiles rewrites', async () => {
|
|
const browser = await next.browser(`/foo${trailingSlash ? '/' : ''}`)
|
|
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(`[href="/photos${trailingSlash ? '/' : ''}"]`)
|
|
.click()
|
|
.waitForElementByCss('#intercepted')
|
|
.text(),
|
|
'intercepted'
|
|
)
|
|
})
|
|
|
|
it('should support intercepting local dynamic sibling routes', async () => {
|
|
const browser = await next.browser(
|
|
`/intercepting-siblings${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-siblings/1${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#intercepted-sibling')
|
|
.text(),
|
|
'1'
|
|
)
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-siblings/2${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#intercepted-sibling')
|
|
.text(),
|
|
'2'
|
|
)
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss(
|
|
`[href="/intercepting-siblings/3${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
.waitForElementByCss('#intercepted-sibling')
|
|
.text(),
|
|
'3'
|
|
)
|
|
|
|
await next.browser(
|
|
`/intercepting-siblings/1${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
await check(() => browser.waitForElementByCss('#main-slot').text(), '1')
|
|
})
|
|
|
|
it('should intercept on routes that contain hyphenated/special dynamic params', async () => {
|
|
const browser = await next.browser(
|
|
`/interception-route-special-params/some-random-param${trailingSlash ? '/' : ''}`
|
|
)
|
|
|
|
await browser
|
|
.elementByCss(
|
|
`[href="/interception-route-special-params/some-random-param/some-page${trailingSlash ? '/' : ''}"]`
|
|
)
|
|
.click()
|
|
|
|
const interceptionText =
|
|
'Hello from [this-is-my-route]/@intercept/some-page. Param: some-random-param'
|
|
const pageText =
|
|
'Hello from [this-is-my-route]/some-page. Param: some-random-param'
|
|
|
|
await retry(async () => {
|
|
expect(await browser.elementByCss('body').text()).toContain(
|
|
interceptionText
|
|
)
|
|
|
|
expect(await browser.elementByCss('body').text()).not.toContain(
|
|
pageText
|
|
)
|
|
})
|
|
|
|
await browser.refresh()
|
|
|
|
await retry(async () => {
|
|
expect(await browser.elementByCss('body').text()).toContain(pageText)
|
|
|
|
expect(await browser.elementByCss('body').text()).not.toContain(
|
|
interceptionText
|
|
)
|
|
})
|
|
})
|
|
|
|
if (isNextStart) {
|
|
it('should not have /default paths in the prerender manifest', async () => {
|
|
const prerenderManifest = JSON.parse(
|
|
await next.readFile('.next/prerender-manifest.json')
|
|
)
|
|
|
|
const routes = Object.keys(prerenderManifest.routes)
|
|
|
|
for (const route of routes) {
|
|
expect(route.endsWith('/default')).toBe(false)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
)
|
|
|
|
describe('parallel-routes-and-interception-conflicting-pages', () => {
|
|
const { next, skipped } = nextTestSetup({
|
|
// This is skipped when deployed as it appears to cause an issue when tracing Next.js files
|
|
// TODO: Investigate why this causes an issue when deployed
|
|
skipDeployment: true,
|
|
files: {
|
|
app: new FileRef(path.join(__dirname, 'app')),
|
|
'app/parallel/nested-2/page.js': `
|
|
export default function Page() {
|
|
return 'hello world'
|
|
}
|
|
`,
|
|
},
|
|
nextConfig,
|
|
})
|
|
|
|
if (skipped) return
|
|
|
|
it('should gracefully handle when two page segments match the `children` parallel slot', async () => {
|
|
const html = await next.render('/parallel/nested-2')
|
|
|
|
// before adding this file, the page would have matched `/app/parallel/(new)/@baz/nested-2/page`
|
|
// but we've added a more specific page, so it should match that instead
|
|
if (process.env.IS_TURBOPACK_TEST) {
|
|
// TODO: this matches differently in Turbopack because the Webpack loader does some sorting on the paths
|
|
// Investigate the discrepancy in a follow-up. For now, since no errors are being thrown (and since this test was previously ignored in Turbopack),
|
|
// we'll just verify that the page is rendered and some content was matched.
|
|
expect(html).toContain('parallel/(new)/@baz/nested/page')
|
|
} else {
|
|
expect(html).toContain('hello world')
|
|
}
|
|
})
|
|
})
|