From 8754d175171967767f4e5530e070d79fd0309498 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 3 Dec 2025 13:29:50 -0700 Subject: [PATCH] fix(router): resolve interception routes with trailing slash configuration Interception routes were not working correctly when the trailingSlash config option was enabled. This was because they were marked as internal routes and bypassed the modifyRouteRegex processing that handles trailing slash normalization. Changes: - Remove internal: true flag from interception route rewrites to ensure they go through proper regex modification - Refactor build-custom-route.ts to scope the source variable closer to its usage point - Add comprehensive test coverage for both trailingSlash true/false configurations - Move next.config.js inline to test file for better test organization --- packages/next/src/lib/build-custom-route.ts | 16 +- .../generate-interception-routes-rewrites.ts | 1 - .../next.config.js | 21 - .../parallel-routes-and-interception.test.ts | 1715 +++++++++-------- 4 files changed, 933 insertions(+), 820 deletions(-) delete mode 100644 test/e2e/app-dir/parallel-routes-and-interception/next.config.js diff --git a/packages/next/src/lib/build-custom-route.ts b/packages/next/src/lib/build-custom-route.ts index 78d5d41212a3a..0c7e558d5d550 100644 --- a/packages/next/src/lib/build-custom-route.ts +++ b/packages/next/src/lib/build-custom-route.ts @@ -37,14 +37,6 @@ export function buildCustomRoute( delimiter: '/', // default is `/#?`, but Next does not pass query info }) - let source = compiled.source - if (!route.internal) { - source = modifyRouteRegex( - source, - type === 'redirect' ? restrictedRedirectPaths : undefined - ) - } - // If this is an internal rewrite and it already provides a regex, use it // otherwise, normalize the source to a regex. let regex: string @@ -54,6 +46,14 @@ export function buildCustomRoute( !('regex' in route) || typeof route.regex !== 'string' ) { + let source = compiled.source + if (!route.internal) { + source = modifyRouteRegex( + source, + type === 'redirect' ? restrictedRedirectPaths : undefined + ) + } + regex = normalizeRouteRegex(source) } else { regex = route.regex diff --git a/packages/next/src/lib/generate-interception-routes-rewrites.ts b/packages/next/src/lib/generate-interception-routes-rewrites.ts index c196b64dbf2fe..90de10fab1abc 100644 --- a/packages/next/src/lib/generate-interception-routes-rewrites.ts +++ b/packages/next/src/lib/generate-interception-routes-rewrites.ts @@ -52,7 +52,6 @@ export function generateInterceptionRoutesRewrites( value: headerRegex, }, ], - internal: true, regex: source.namedRegex, }) } diff --git a/test/e2e/app-dir/parallel-routes-and-interception/next.config.js b/test/e2e/app-dir/parallel-routes-and-interception/next.config.js deleted file mode 100644 index 87591b02febda..0000000000000 --- a/test/e2e/app-dir/parallel-routes-and-interception/next.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const nextConfig = { - async rewrites() { - return { - beforeFiles: [ - { - source: '/foo', - destination: '/en/foo', - }, - { - source: '/photos', - destination: '/en/photos', - }, - ], - } - }, -} - -module.exports = nextConfig diff --git a/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts b/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts index 6698791a4f273..4e5c31590c375 100644 --- a/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts +++ b/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts @@ -1,926 +1,1060 @@ import { nextTestSetup, FileRef } from 'e2e-utils' +import { NextConfig } from 'next' import { check, retry } from 'next-test-utils' import path from 'path' -describe('parallel-routes-and-interception', () => { - const { next, isNextDev, isNextStart } = nextTestSetup({ - files: __dirname, - }) +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') - describe('parallel routes', () => { - it('should support parallel route tab bars', async () => { - const browser = await next.browser('/parallel-tab-bar') + // 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') + }) - const hasHome = async () => { + 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)' ) - } - const hasViewsHome = async () => { - await check( - () => browser.waitForElementByCss('#views-home').text(), - 'Views home' - ) - } - const hasViewDuration = async () => { + await browser.elementByCss('#view-duration-link').click() await check( () => browser.waitForElementByCss('#view-duration').text(), 'View duration' ) - } - const hasImpressions = async () => { - await check( - () => browser.waitForElementByCss('#impressions').text(), - 'Impressions' + + // fetch /parallel-tab-bar/view-duration + const res = await next.fetch( + `${next.url}/parallel-tab-bar/view-duration` ) - } - const hasAudienceHome = async () => { + 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('#audience-home').text(), - 'Audience home' + () => browser.waitForElementByCss('#nested-deeper-main').text(), + 'Nested deeper page' ) - } - const hasDemographics = async () => { + await check( - () => browser.waitForElementByCss('#demographics').text(), - 'Demographics' + () => browser.waitForElementByCss('#nested-deeper-sidebar').text(), + 'Nested deeper sidebar here' ) - } - const hasSubscribers = async () => { + + await browser + .elementByCss( + `[href="/parallel-side-bar/nested${trailingSlash ? '/' : ''}"]` + ) + .click() + await check( - () => browser.waitForElementByCss('#subscribers').text(), - 'Subscribers' + () => browser.waitForElementByCss('#nested-main').text(), + 'Nested page' ) - } - const checkUrlPath = async (path: string) => { - await check(() => browser.url(), `${next.url}/parallel-tab-bar${path}`) - } - // Initial page - const step1 = async () => { - await hasHome() - await hasViewsHome() - await hasAudienceHome() - await checkUrlPath('') - } + await check( + () => browser.waitForElementByCss('#nested-sidebar').text(), + 'Nested sidebar here' + ) - await step1() + await browser + .elementByCss( + `[href="/parallel-side-bar${trailingSlash ? '/' : ''}"]` + ) + .click() - console.log('step1') - // Navigate to /views/duration - await browser.elementByCss('#view-duration-link').click() + await check( + () => browser.waitForElementByCss('#main').text(), + 'homepage' + ) - const step2 = async () => { - await hasHome() - await hasViewDuration() - await hasAudienceHome() - await checkUrlPath('/view-duration') - } + await check( + () => browser.waitForElementByCss('#sidebar-main').text(), + 'root sidebar here' + ) + }) - await step2() - console.log('step2') + 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 /views/impressions - await browser.elementByCss('#impressions-link').click() + // 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' + ) + }) - const step3 = async () => { - await hasHome() - await hasImpressions() - await hasAudienceHome() - await checkUrlPath('/impressions') - } + it('should only scroll to the parallel route that was navigated to', async () => { + const browser = await next.browser('/parallel-scroll') - await step3() - console.log('step3') + 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) + }) - // Navigate to /audience/demographics - await browser.elementByCss('#demographics-link').click() + 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') - const step4 = async () => { - await hasHome() - await hasImpressions() - await hasDemographics() - await checkUrlPath('/demographics') - } + 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 step4() - console.log('step4') + 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' + ) - // Navigate to /audience/subscribers - await browser.elementByCss('#subscribers-link').click() + 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' + ) + }) - const step5 = async () => { - await hasHome() - await hasImpressions() - await hasSubscribers() - await checkUrlPath('/subscribers') - } + 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 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() - }) + 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' + ) - 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') - }) + 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' + ) - 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') - }) + 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 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 navigate with a link with prefetch=false', async () => { + const browser = await next.browser('/parallel-prefetch-false') - 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' - ) + // check if the default view loads + await check( + () => browser.waitForElementByCss('#default-parallel').text(), + 'default view for parallel' + ) - await check( - () => browser.waitForElementByCss('#nested-deeper-sidebar').text(), - 'Nested deeper sidebar here' - ) + // 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' + ) + }) - await browser.elementByCss('[href="/parallel-side-bar/nested"]').click() + it('should display all parallel route params with useParams', async () => { + const browser = await next.browser('/parallel-dynamic/foo/bar') - await check( - () => browser.waitForElementByCss('#nested-main').text(), - 'Nested page' - ) + await check( + () => browser.waitForElementByCss('#foo').text(), + `{"slug":"foo","id":"bar"}` + ) - await check( - () => browser.waitForElementByCss('#nested-sidebar').text(), - 'Nested sidebar here' - ) + await check( + () => browser.waitForElementByCss('#bar').text(), + `{"slug":"foo","id":"bar"}` + ) + }) - await browser.elementByCss('[href="/parallel-side-bar"]').click() + it('should load CSS for a default page that exports another page', async () => { + const browser = await next.browser('/default-css') - await check(() => browser.waitForElementByCss('#main').text(), 'homepage') + expect( + await browser.eval( + `window.getComputedStyle(document.getElementById("red-text")).color` + ) + ).toBe('rgb(255, 0, 0)') - await check( - () => browser.waitForElementByCss('#sidebar-main').text(), - 'root sidebar here' - ) - }) + // 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() - 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"]').click() - await check( - () => browser.waitForElementByCss('#parallel-layout').text(), - 'parallel layout' - ) - await check( - () => browser.waitForElementByCss('#parallel-subroute').text(), - 'parallel subroute layout' - ) - }) + expect( + await browser.eval( + `window.getComputedStyle(document.getElementById("red-text")).color` + ) + ).toBe('rgb(255, 0, 0)') - it('should only scroll to the parallel route that was navigated to', async () => { - const browser = await next.browser('/parallel-scroll') + // ensure that everything still works on a fresh load + await browser.refresh() - 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"]').click() - await browser.waitForElementByCss('#modal') - // check that we didn't scroll back to the top - await check(() => browser.eval('window.scrollY'), position) - }) + expect( + await browser.eval( + `window.getComputedStyle(document.getElementById("red-text")).color` + ) + ).toBe('rgb(255, 0, 0)') + }) - 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"]').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"]').click() - await check(() => browser.waitForElementByCss('#main').text(), 'foo') - await check( - () => browser.waitForElementByCss('#slot-content').text(), - 'foo slot' - ) - - await browser.elementByCss('[href="/parallel-catchall/baz"]').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 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') - 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"]') - .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"]') - .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"]') - .click() - await check(() => browser.waitForElementByCss('#main').text(), 'foo id') - await check( - () => browser.waitForElementByCss('#slot-content').text(), - 'foo id catchAll' - ) - }) + // 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' + ) + }) + }) - it('should navigate with a link with prefetch=false', async () => { - const browser = await next.browser('/parallel-prefetch-false') + 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() - // check if the default view loads - await check( - () => browser.waitForElementByCss('#default-parallel').text(), - 'default view for parallel' - ) + await new Promise((resolve) => { + setTimeout(resolve, 3000) + }) - // check that navigating to /foo re-renders the layout to display @parallel/foo - await check( - () => - browser - .elementByCss('[href="/parallel-prefetch-false/foo"]') - .click() - .waitForElementByCss('#parallel-foo') - .text(), - 'parallel for foo' - ) - }) + 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 display all parallel route params with useParams', async () => { - const browser = await next.browser('/parallel-dynamic/foo/bar') + it('should support nested parallel routes', async () => { + const browser = await next.browser('parallel-nested/home/nested') + const timestamp = await browser.elementByCss('#timestamp').text() - await check( - () => browser.waitForElementByCss('#foo').text(), - `{"slug":"foo","id":"bar"}` - ) + await new Promise((resolve) => { + setTimeout(resolve, 3000) + }) - await check( - () => browser.waitForElementByCss('#bar').text(), - `{"slug":"foo","id":"bar"}` - ) + 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 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` + describe('route intercepting with dynamic routes', () => { + it('should render intercepted route', async () => { + const browser = await next.browser( + `/intercepting-routes-dynamic/photos${trailingSlash ? '/' : ''}` ) - ).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"]').click() + // 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' + ) - expect( - await browser.eval( - `window.getComputedStyle(document.getElementById("red-text")).color` + // Check if url matches even though it was intercepted. + await check( + () => browser.url(), + next.url + + '/intercepting-routes-dynamic/photos/next/123' + + (trailingSlash ? '/' : '') ) - ).toBe('rgb(255, 0, 0)') - // ensure that everything still works on a fresh load - await browser.refresh() + // Trigger a refresh, this should load the normal page, not the modal. + await check( + () => + browser.refresh().waitForElementByCss('#user-regular-page').text(), + 'Regular Page' + ) - expect( - await browser.eval( - `window.getComputedStyle(document.getElementById("red-text")).color` + // Check if the url matches still. + await check( + () => browser.url(), + next.url + + '/intercepting-routes-dynamic/photos/next/123' + + (trailingSlash ? '/' : '') ) - ).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') + describe('route intercepting with prerendered dynamic routes ', () => { + it('should render intercepted route', async () => { + const browser = await next.browser( + '/intercepting-routes-dynamic-prerendered/photos' + ) - // should have triggered a loading state - expect( + // Check if navigation to modal route works. await browser - .elementByCss('[href="/with-loading/foo"]') + .elementByCss( + `[href="/intercepting-routes-dynamic-prerendered/photos/1${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' + // 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 ? '/' : '') ) - }) - }) - 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() + // There must not be any errors from prefetching the intercepted page. + expect( + (await browser.log()).filter(({ source }) => source === 'error') + ).toEqual([]) - await new Promise((resolve) => { - setTimeout(resolve, 3000) - }) + // 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' + ) - 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') + // Check if the url matches still. + expect(await browser.url()).toBe( + next.url + + '/intercepting-routes-dynamic-prerendered/photos/1' + + (trailingSlash ? '/' : '') + ) }) + }) - it('should support nested parallel routes', async () => { - const browser = await next.browser('parallel-nested/home/nested') - const timestamp = await browser.elementByCss('#timestamp').text() + 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 ? '/' : ''}` + ) - await new Promise((resolve) => { - setTimeout(resolve, 3000) - }) + // 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' + ) - 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') - }) - } - }) + // Check if url matches even though it was intercepted. + await check( + () => browser.url(), + next.url + + '/intercepting-routes-dynamic-catchall/photos/optional-catchall/123' + + (trailingSlash ? '/' : '') + ) - describe('route intercepting with dynamic routes', () => { - it('should render intercepted route', async () => { - const browser = await next.browser('/intercepting-routes-dynamic/photos') + // 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 navigation to modal route works - await check( - () => - browser - .elementByCss( - '[href="/intercepting-routes-dynamic/photos/next/123"]' - ) - .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' - ) - - // 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' - ) + // 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 prerendered dynamic routes ', () => { - it('should render intercepted route', async () => { - const browser = await next.browser( - '/intercepting-routes-dynamic-prerendered/photos' - ) + 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 browser - .elementByCss( - '[href="/intercepting-routes-dynamic-prerendered/photos/1"]' + // 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' ) - .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. + await check( + () => browser.url(), + next.url + + '/intercepting-routes-dynamic-catchall/photos/catchall/123' + + (trailingSlash ? '/' : '') + ) - // Check if url matches even though it was intercepted. - expect(await browser.url()).toBe( - next.url + '/intercepting-routes-dynamic-prerendered/photos/1' - ) - - // 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' - ) + // 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 with dynamic optional catch-all routes', () => { - it('should render intercepted route', async () => { - const browser = await next.browser( - '/intercepting-routes-dynamic-catchall/photos' - ) + 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-dynamic-catchall/photos/optional-catchall/123"]' - ) - .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' - ) - - // 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' - ) - }) - }) + // 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' + ) - describe('route intercepting with dynamic catch-all routes', () => { - it('should render intercepted route', async () => { - const browser = await next.browser( - '/intercepting-routes-dynamic-catchall/photos' - ) + // 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 navigation to modal route works - await check( - () => - browser - .elementByCss( - '[href="/intercepting-routes-dynamic-catchall/photos/catchall/123"]' - ) - .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' - ) - - // 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' - ) - }) - }) + // Check if url matches even though it was intercepted. + await check( + () => browser.url(), + next.url + + '/intercepting-routes/feed/photos/1' + + (trailingSlash ? '/' : '') + ) - describe('route intercepting', () => { - it('should render intercepted route', async () => { - const browser = await next.browser('/intercepting-routes/feed') + // 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 navigation to modal route works. - await check( - () => - browser - .elementByCss('[href="/intercepting-routes/feed/photos/1"]') - .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' - ) - - // 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' - ) - }) + // 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('/') + 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.waitForElementByCss('#default-slot').text(), + 'default from @slot' + ) - await check( - () => - browser - .elementByCss('[href="/nested"]') - .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' - ) - }) + await check( + () => + browser + .elementByCss(`[href="/nested${trailingSlash ? '/' : ''}"]`) + .click() + .waitForElementByCss('#interception-slot') + .text(), + 'interception from @slot/nested' + ) - it('should render an intercepted route at the top level from a nested path', async () => { - const browser = await next.browser('/nested-link') + // Check if the client component is rendered + await check( + () => browser.waitForElementByCss('#interception-slot-client').text(), + 'client component' + ) - await check( - () => browser.waitForElementByCss('#default-slot').text(), - 'default from @slot' - ) + await check( + () => browser.refresh().waitForElementByCss('#nested').text(), + 'hello world from /nested' + ) + }) - await check( - () => - browser - .elementByCss('[href="/nested"]') - .click() - .waitForElementByCss('#interception-slot') - .text(), - 'interception from @slot/nested' - ) - - 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 ? '/' : ''}` + ) - it('should render intercepted route from a nested route', async () => { - const browser = await next.browser('/intercepting-routes/feed/nested') + await check( + () => browser.waitForElementByCss('#default-slot').text(), + 'default from @slot' + ) - // Check if navigation to modal route works. - await check( - () => - browser - .elementByCss('[href="/intercepting-routes/feed/photos/1"]') - .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' - ) - - // 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' - ) - }) + await check( + () => + browser + .elementByCss(`[href="/nested${trailingSlash ? '/' : ''}"]`) + .click() + .waitForElementByCss('#interception-slot') + .text(), + 'interception from @slot/nested' + ) - 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') + await check( + () => browser.refresh().waitForElementByCss('#nested').text(), + 'hello world from /nested' + ) + }) - // check if the default view loads - await check( - () => browser.waitForElementByCss('#default-parallel').text(), - 'default view for parallel' - ) + it('should render intercepted route from a nested route', async () => { + const browser = await next.browser( + `/intercepting-routes/feed/nested${trailingSlash ? '/' : ''}` + ) - // check that navigating to /foo re-renders the layout to display @parallel/foo - await check( - () => - browser - .elementByCss('[href="/parallel-non-intercepting/foo"]') - .click() - .waitForElementByCss('#parallel-foo') - .text(), - 'parallel for foo' - ) + // 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 that navigating to /foo also re-renders the base children - await check(() => browser.elementByCss('#children-foo').text(), 'foo') - }) + // 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') - it('should render modal when paired with parallel routes', async () => { - const browser = await next.browser('/intercepting-parallel-modal/vercel') - // Check if navigation to modal route works. - await check( - () => - browser - .elementByCss('[href="/intercepting-parallel-modal/photo/1"]') - .click() - .waitForElementByCss('#photo-modal-1') - .text(), - 'Photo MODAL 1' - ) - - await check( - () => - browser - .elementByCss('[href="/intercepting-parallel-modal/photo/2"]') - .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' - ) - - // 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' - ) - }) + // Check if url matches even though it was intercepted. + await check( + () => browser.url(), + next.url + + '/intercepting-routes/feed/photos/1' + + (trailingSlash ? '/' : '') + ) - it('should support intercepting with beforeFiles rewrites', async () => { - const browser = await next.browser('/foo') + // Trigger a refresh, this should load the normal page, not the modal. + await check( + () => browser.refresh().waitForElementByCss('#photo-page-1').text(), + 'Photo PAGE 1' + ) - await check( - () => - browser - .elementByCss('[href="/photos"]') - .click() - .waitForElementByCss('#intercepted') - .text(), - 'intercepted' - ) - }) + // Check if the url matches still. + await check( + () => browser.url(), + next.url + + '/intercepting-routes/feed/photos/1' + + (trailingSlash ? '/' : '') + ) + }) - it('should support intercepting local dynamic sibling routes', async () => { - const browser = await next.browser('/intercepting-siblings') + 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 ? '/' : ''}` + ) - await check( - () => - browser - .elementByCss('[href="/intercepting-siblings/1"]') - .click() - .waitForElementByCss('#intercepted-sibling') - .text(), - '1' - ) - await check( - () => - browser - .elementByCss('[href="/intercepting-siblings/2"]') - .click() - .waitForElementByCss('#intercepted-sibling') - .text(), - '2' - ) - await check( - () => - browser - .elementByCss('[href="/intercepting-siblings/3"]') - .click() - .waitForElementByCss('#intercepted-sibling') - .text(), - '3' - ) + // check if the default view loads + await check( + () => browser.waitForElementByCss('#default-parallel').text(), + 'default view for parallel' + ) - await next.browser('/intercepting-siblings/1') + // 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' + ) - await check(() => browser.waitForElementByCss('#main-slot').text(), '1') - }) + // check that navigating to /foo also re-renders the base children + await check(() => browser.elementByCss('#children-foo').text(), 'foo') + }) - it('should intercept on routes that contain hyphenated/special dynamic params', async () => { - const browser = await next.browser( - '/interception-route-special-params/some-random-param' - ) + 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 browser - .elementByCss( - "[href='/interception-route-special-params/some-random-param/some-page']" + await check( + () => + browser + .elementByCss( + `[href="/intercepting-parallel-modal/photo/2${trailingSlash ? '/' : ''}"]` + ) + .click() + .waitForElementByCss('#photo-modal-2') + .text(), + 'Photo MODAL 2' ) - .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' + // 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' + ) - await retry(async () => { - expect(await browser.elementByCss('body').text()).toContain( - interceptionText + // 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 ? '/' : ''}`) - expect(await browser.elementByCss('body').text()).not.toContain( - pageText + await check( + () => + browser + .elementByCss(`[href="/photos${trailingSlash ? '/' : ''}"]`) + .click() + .waitForElementByCss('#intercepted') + .text(), + 'intercepted' ) }) - await browser.refresh() + it('should support intercepting local dynamic sibling routes', async () => { + const browser = await next.browser( + `/intercepting-siblings${trailingSlash ? '/' : ''}` + ) - await retry(async () => { - expect(await browser.elementByCss('body').text()).toContain(pageText) + 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' + ) - expect(await browser.elementByCss('body').text()).not.toContain( - interceptionText + await next.browser( + `/intercepting-siblings/1${trailingSlash ? '/' : ''}` ) + + await check(() => browser.waitForElementByCss('#main-slot').text(), '1') }) - }) - if (isNextStart) { - it('should not have /default paths in the prerender manifest', async () => { - const prerenderManifest = JSON.parse( - await next.readFile('.next/prerender-manifest.json') + 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 ? '/' : ''}` ) - const routes = Object.keys(prerenderManifest.routes) + await browser + .elementByCss( + `[href="/interception-route-special-params/some-random-param/some-page${trailingSlash ? '/' : ''}"]` + ) + .click() - for (const route of routes) { - expect(route.endsWith('/default')).toBe(false) - } + 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({ @@ -935,6 +1069,7 @@ describe('parallel-routes-and-interception-conflicting-pages', () => { } `, }, + nextConfig, }) if (skipped) return