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,7 @@
export default function Page() {
return 'intercepted'
}
export async function generateStaticParams() {
return [{ username: 'john', id: '1' }]
}

View File

@@ -0,0 +1,8 @@
export default function Page() {
return (
<div>
<h3>Deeper page under explicit layout</h3>
<p>This page is nested under the explicit layout</p>
</div>
)
}

View File

@@ -0,0 +1,18 @@
/**
* TEST CASE: Interception route WITH explicit layout.tsx but NO parallel routes
*
* This is the KEY test to determine if we need to check for:
* - Implicit layout (from parallel routes) OR
* - Explicit layout (layout.tsx file)
*/
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<div>Explicit layout at (.)explicit-layout</div>
<div>
<div>children:</div>
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div>
<strong>@sidebar content</strong>
</div>
)
}

View File

@@ -0,0 +1,17 @@
/**
* TEST CASE 3: Interception route WITH parallel routes AND page.tsx
*
* Expected: NO default.tsx needed for children slot
* Reason: Has page.tsx which fills the children slot
* The @sidebar slot exists but children slot has content
*/
export default function Page() {
return (
<div>
<h3> TEST CASE 3: Has both @sidebar AND page.tsx</h3>
<p>This level has @sidebar parallel route AND a page.tsx</p>
<p>The page.tsx fills the children slot.</p>
<p>NO default.tsx needed!</p>
</div>
)
}

View File

@@ -0,0 +1,15 @@
/**
* TEST CASE 1: Interception route WITH page.tsx
*
* Expected: NO default.tsx needed
* Reason: Has a page at this level, so no children slot exists
*/
export default function Page() {
return (
<div>
<h3> TEST CASE 1: Has page.tsx</h3>
<p>This interception route has a page.tsx at this level.</p>
<p>No children slot exists, so NO default.tsx needed!</p>
</div>
)
}

View File

@@ -0,0 +1,17 @@
/**
* TEST CASE 2: Interception route WITHOUT parallel routes
*
* Expected: NO default.tsx needed at (.)no-parallel-routes level
* Reason: No @parallel routes exist, so no implicit layout created
* Only has a nested page, no children slot at root level
*/
export default function Page() {
return (
<div>
<h3> TEST CASE 2: No parallel routes</h3>
<p>This is a nested page under (.)no-parallel-routes/deeper</p>
<p>No @sidebar or other parallel routes exist at parent level.</p>
<p>NO default.tsx needed at (.)no-parallel-routes!</p>
</div>
)
}

View File

@@ -0,0 +1,17 @@
/**
* TEST CASE: Interception route WITHOUT parallel routes (just a page)
*
* Expected: Should work WITHOUT special null default handling
* Reason: No parallel routes at this level means no children slot structure
* The page itself is the content, no implicit layout needed
*/
export default function Page() {
return (
<div>
<h3> Simple interception page (no parallel routes)</h3>
<p>This is just a regular page.tsx at the interception route level.</p>
<p>No @sidebar or other parallel routes.</p>
<p>Should NOT need null default logic!</p>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Intercepted test-nested sidebar/deeper</div>
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Intercepted test-nested sidebar</div>
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Panel at deeper level (deeper than marker)</div>
}

View File

@@ -0,0 +1,14 @@
export default function Layout({
children,
sidebar,
}: {
children: React.ReactNode
sidebar: React.ReactNode
}) {
return (
<div>
<div>{sidebar}</div>
<div>{children}</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function CatchAll() {
return 'catch-all'
}

View File

@@ -0,0 +1 @@
export default () => 'default'

View File

@@ -0,0 +1,3 @@
export default function Page() {
return 'intercepted'
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return 'not intercepted'
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Regular explicit-layout/deeper route</div>
}

View File

@@ -0,0 +1,8 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<div>Regular explicit-layout layout</div>
{children}
</div>
)
}

View File

@@ -0,0 +1,19 @@
type Props = {
params: Promise<{ slug: string }>
}
export default async function Page({ params }: Props) {
const { slug } = await params
return <div>Hello {slug}</div>
}
export function generateStaticParams() {
return [
{ slug: 'a' },
{ slug: 'b' },
{ slug: 'c' },
{ slug: 'd' },
{ slug: 'e' },
{ slug: 'f' },
]
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Sidebar for regular has-both route</div>
}

View File

@@ -0,0 +1,21 @@
export default function Layout({
children,
sidebar,
}: {
children: React.ReactNode
sidebar: React.ReactNode
}) {
return (
<div>
<div>has-both layout</div>
<div>
<div>@sidebar:</div>
{sidebar}
</div>
<div>
<div>children:</div>
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Regular has-both route</div>
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Regular has-page route</div>
}

View File

@@ -0,0 +1,19 @@
export default function Layout(props: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
<div id="children">
<div>CHILDREN SLOT:</div>
{props.children}
</div>
<div id="modal">
<div>MODAL SLOT:</div>
{props.modal}
</div>
</body>
</html>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Regular no-parallel-routes/deeper route</div>
}

View File

@@ -0,0 +1,94 @@
'use client'
import Link from 'next/link'
import { useState } from 'react'
export function LinkAccordion({ href }: { href: string }) {
const [isOpen, setIsOpen] = useState(false)
return (
<div
data-testid="link-accordion"
data-href={href}
style={{
border: '1px solid #e5e7eb',
borderRadius: '12px',
padding: '16px',
margin: '12px 0',
backgroundColor: 'white',
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
transition: 'all 0.2s ease',
cursor: 'pointer',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '12px',
}}
>
<div
style={{
fontFamily: 'monospace',
fontSize: '14px',
color: '#374151',
fontWeight: '500',
flex: 1,
}}
>
{href}
</div>
{!isOpen && (
<button
onClick={() => setIsOpen(true)}
style={{
padding: '8px 16px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#3b82f6',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<span></span>
Open
</button>
)}
</div>
{isOpen && (
<div
style={{
marginTop: '16px',
paddingTop: '16px',
borderTop: '1px solid #e5e7eb',
}}
>
<Link
href={href}
style={{
display: 'inline-block',
padding: '10px 20px',
backgroundColor: '#10b981',
color: 'white',
textDecoration: 'none',
borderRadius: '8px',
fontSize: '14px',
fontWeight: '600',
}}
>
Navigate to {href}
</Link>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,83 @@
import { LinkAccordion } from './page.client'
export default function Page() {
return (
<div id="children">
<section>
<h2> Should Work WITHOUT default.tsx</h2>
<div>
<h3>Test Case 1a: Simple page (no parallel routes)</h3>
<LinkAccordion href="/simple-page" />
<p>
Interception route with just a page.tsx, no parallel routes at all.
</p>
</div>
<div>
<h3>Test Case 1b: Has page.tsx</h3>
<LinkAccordion href="/has-page" />
<p>
Interception route has page.tsx at root level. No children slot
exists.
</p>
</div>
<div>
<h3>Test Case 2: No parallel routes</h3>
<LinkAccordion href="/no-parallel-routes/deeper" />
<p>No @parallel routes exist, so no implicit layout created.</p>
</div>
<div>
<h3>Test Case 3: Has both @sidebar AND page.tsx</h3>
<LinkAccordion href="/has-both" />
<p>
Has @sidebar parallel route BUT page.tsx fills the children slot.
</p>
</div>
</section>
<section>
<h2>🔬 Test Cases - Require Null Default Logic</h2>
<div>
<h3>Test Case 4a: Has @sidebar but NO page.tsx (implicit layout)</h3>
<LinkAccordion href="/test-nested" />
<p>Has @sidebar (creates implicit layout) but NO page.tsx.</p>
<p> Auto-uses null default (no explicit files needed)</p>
</div>
<div>
<h3>Test Case 4b: Has explicit layout.tsx but NO parallel routes</h3>
<LinkAccordion href="/explicit-layout/deeper" />
<p>
Has explicit layout.tsx with children slot, but NO parallel routes
like @sidebar.
</p>
<p>
? Should it 404 or 200? This determines if we need to check for
explicit layouts!
</p>
</div>
</section>
<section>
<h2>Original Tests</h2>
<ul>
<li>
<LinkAccordion href="/foo/1" />
</li>
<li>
<LinkAccordion href="/bar/1" />
</li>
<li>
<LinkAccordion href="/test-nested/deeper" />
</li>
<li>
<LinkAccordion href="/generate-static-params/foo" />
</li>
</ul>
</section>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div>
<strong>Regular route @sidebar</strong>
</div>
)
}

View File

@@ -0,0 +1,18 @@
/**
* TEST CASE 5: Regular route (NO interception) WITH parallel routes but NO page.tsx
*
* Expected: REQUIRES default.tsx at regular-route level OR will 404
* Reason: This is NOT an interception route, so missing children should 404
* Currently uses PARALLEL_ROUTE_DEFAULT_PATH (calls notFound())
* This is the CORRECT behavior for non-interception routes
*/
export default function Page() {
return (
<div>
<h3> TEST CASE 5: Regular route without default.tsx</h3>
<p>This is deeper/page.tsx</p>
<p>Parent has @sidebar but no page.tsx or default.tsx</p>
<p>Should 404 when navigating directly to /regular-route!</p>
</div>
)
}

View File

@@ -0,0 +1,15 @@
export default function Layout({
children,
sidebar,
}: {
children: React.ReactNode
sidebar: React.ReactNode
}) {
return (
<div>
<div>Regular Route Layout</div>
<div>{sidebar}</div>
<div>{children}</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Regular simple-page route</div>
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Actual test-nested sidebar/deeper</div>
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Actual test-nested sidebar</div>
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Actual test-nested/deeper page</div>
}

View File

@@ -0,0 +1,21 @@
export default function Layout({
children,
sidebar,
}: {
children: React.ReactNode
sidebar: React.ReactNode
}) {
return (
<div>
<div>test-nested layout</div>
<div>
<div>@sidebar slot:</div>
{sidebar}
</div>
<div>
<div>children slot:</div>
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Actual test-nested page</div>
}

View File

@@ -0,0 +1,418 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import { Playwright } from 'next-webdriver'
import { createRouterAct } from 'router-act'
describe('interception-dynamic-segment', () => {
const { next, isNextStart, isNextDev } = nextTestSetup({
files: __dirname,
})
/**
* Returns true if the given href should already be opened. This allows us to
* condition on whether to expect any additional network requests.
*/
async function isAccordionClosed(
browser: Playwright,
href: string
): Promise<boolean> {
const selector = `[data-testid="link-accordion"][data-href="${href}"]`
// Check if the button is already open
return await browser.hasElementByCss(`${selector} button`)
}
/**
* Helper to navigate via the LinkAccordion component.
* Scrolls to the accordion, opens it, and clicks the link.
*/
async function navigate(browser: Playwright, href: string) {
const selector = `[data-testid="link-accordion"][data-href="${href}"]`
// Find and scroll to accordion
const accordion = await browser.elementByCss(selector)
await accordion.scrollIntoViewIfNeeded()
// Click the "Open" button, it may already be open, so we don't need to
// click it again.
if (await isAccordionClosed(browser, href)) {
const button = await browser.elementByCss(`${selector} button`)
await button.click()
}
// Click the actual link
const link = await browser.elementByCss(`${selector} a`)
await link.click()
}
/**
* Create a browser with router act that will FAIL if any 404s occur during navigation.
* This is critical because if a 404 occurs, the client will perform MPA navigation
* (full page reload) which still successfully navigates, hiding the bug.
*/
async function createBrowserWithRouterAct(url: string) {
let act: ReturnType<typeof createRouterAct>
const browser = await next.browser(url, {
beforePageLoad(page) {
// DON'T use allowErrorStatusCodes - we want 404s to fail the test
act = createRouterAct(page)
},
})
return { act: act!, browser }
}
it('should work when interception route is paired with a dynamic segment', async () => {
const browser = await next.browser('/')
await navigate(browser, '/foo/1')
await browser.waitForIdleNetwork()
await retry(async () => {
expect(await browser.elementById('modal').text()).toContain('intercepted')
})
await browser.refresh()
await browser.waitForIdleNetwork()
await retry(async () => {
expect(await browser.elementById('modal').text()).toContain('catch-all')
})
await retry(async () => {
expect(await browser.elementById('children').text()).toContain(
'not intercepted'
)
})
})
it('should intercept consistently with back/forward navigation', async () => {
// Test that the fix works with browser back/forward navigation
const browser = await next.browser('/')
// Navigate with interception
await navigate(browser, '/foo/1')
await browser.waitForIdleNetwork()
await retry(async () => {
expect(await browser.elementById('modal').text()).toContain('intercepted')
})
// Go back to root
await browser.back()
await browser.waitForIdleNetwork()
await retry(async () => {
const url = await browser.url()
expect(url).toContain('/')
})
// Go forward - should show intercepted version
await browser.forward()
await browser.waitForIdleNetwork()
await retry(async () => {
expect(await browser.elementById('modal').text()).toContain('intercepted')
})
})
it('should intercept multiple times from root', async () => {
// Test that repeated interception from root works
const browser = await next.browser('/')
for (let i = 0; i < 2; i++) {
await navigate(browser, '/foo/1')
await browser.waitForIdleNetwork()
await retry(async () => {
expect(await browser.elementById('modal').text()).toContain(
'intercepted'
)
})
await browser.back()
await browser.waitForIdleNetwork()
await retry(async () => {
const url = await browser.url()
expect(url).toMatch(/\/$/)
})
}
})
if (isNextStart) {
it('should correctly prerender segments with generateStaticParams', async () => {
expect(next.cliOutput).toContain('/generate-static-params/a')
const res = await next.fetch('/generate-static-params/a')
expect(res.status).toBe(200)
expect(res.headers.get('x-nextjs-cache')).toBe('HIT')
})
it('should prerender a dynamic intercepted route', async () => {
if (process.env.__NEXT_CACHE_COMPONENTS === 'true') {
expect(next.cliOutput).toContain('/(.)[username]/[id]')
expect(next.cliOutput).toContain('/(.)john/[id]')
}
expect(next.cliOutput).toContain('/(.)john/1')
expect(next.cliOutput).not.toContain('/john/1')
})
}
if (!isNextDev) {
/**
* Test Case Validation: Ensure NO 404s occur during interception navigation
* These tests validate the fix for default.tsx injection with parallel routes.
* Using createRouterAct WITHOUT allowErrorStatusCodes ensures that any 404
* response will fail the test, preventing the bug where MPA navigation masks 404s.
*/
describe('Default.tsx injection validation (no 404s allowed)', () => {
/**
* Test Case: Dynamic segment interception route [username]/[id]
* Validates that intercepted routes with dynamic segments don't return 404
*/
it('should not render a 404 for the intercepted route with dynamic segments', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
await act(async () => {
await navigate(browser, '/foo/1')
})
await retry(async () => {
expect(await browser.elementById('modal').text()).toContain(
'intercepted'
)
})
})
/**
* Test Case 1a: Simple interception page (no parallel routes)
* Structure: @modal/(.)simple-page/page.tsx
* Expected: Should work WITHOUT null default logic
* Reason: No parallel routes = no implicit layout = no children slot
*/
it('should navigate to /simple-page without 404 (no parallel routes)', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
await act(async () => {
await navigate(browser, '/simple-page')
})
await retry(async () => {
expect(await browser.elementByCss('#modal h3').text()).toContain(
'Simple interception page'
)
})
})
/**
* Test Case 1b: Has page.tsx at interception level
* Structure: @modal/(.)has-page/page.tsx
* Expected: Should work WITHOUT default.tsx
* Reason: page.tsx fills the children slot
*/
it('should navigate to /has-page without 404 (page fills children)', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
await act(async () => {
await navigate(browser, '/has-page')
})
await retry(async () => {
expect(await browser.elementByCss('#modal h3').text()).toContain(
'TEST CASE 1'
)
})
})
/**
* Test Case 2: No parallel routes (nested page)
* Structure: @modal/(.)no-parallel-routes/deeper/page.tsx
* Expected: Should work WITHOUT default.tsx at parent level
* Reason: No parallel routes exist, so no implicit layout
*/
it('should navigate to /no-parallel-routes/deeper without 404', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
await act(async () => {
await navigate(browser, '/no-parallel-routes/deeper')
})
await retry(async () => {
expect(await browser.elementByCss('#modal h3').text()).toContain(
'TEST CASE 2'
)
})
})
/**
* Test Case 3: Has both @sidebar AND page.tsx
* Structure: @modal/(.)has-both/page.tsx + @sidebar/page.tsx
* Expected: Should work WITHOUT default.tsx
* Reason: page.tsx fills children slot, even though @sidebar creates implicit layout
*/
it('should navigate to /has-both without 404 (has both @sidebar and page)', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
await act(async () => {
await navigate(browser, '/has-both')
})
await retry(async () => {
expect(await browser.elementByCss('#modal h3').text()).toContain(
'TEST CASE 3'
)
})
})
/**
* Test Case 4: Has @sidebar but NO page.tsx (THE KEY BUG CASE)
* Structure: @modal/(.)test-nested/@sidebar/page.tsx (NO page.tsx at root)
* Expected: Should work WITHOUT explicit default.tsx (auto null default)
* Reason: Interception + parallel routes should inject null default
*
* This is the critical test! Without the fix:
* 1. Server returns 404 (default.js calls notFound())
* 2. Client sees !res.ok in fetch-server-response.ts:229
* 3. Client triggers doMpaNavigation() - full page reload
* 4. Navigation still succeeds via MPA, hiding the 404 bug
*
* With createRouterAct (no allowErrorStatusCodes), 404 fails the test.
*/
it('should navigate to /test-nested without 404 (auto null default)', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
await act(async () => {
await navigate(browser, '/test-nested')
})
await retry(async () => {
// Modal should show intercepted content
const modalContent = await browser.elementByCss('#modal').text()
expect(modalContent).toContain('Intercepted test-nested sidebar')
})
await retry(async () => {
// Children slot should still show original page (/)
const childrenContent = await browser.elementByCss('#children').text()
expect(childrenContent).toContain('CHILDREN SLOT')
})
})
/**
* Test Case 4b: Navigate deeper within intercepted route with parallel routes
* This validates that navigating to the deeper page directly (from home) works
*/
it('should navigate to /test-nested/deeper without 404', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
// Navigate directly to the deeper page from home
await act(async () => {
await navigate(browser, '/test-nested/deeper')
})
await retry(async () => {
const modalContent = await browser.elementByCss('#modal').text()
// Should show the deeper intercepted content
expect(modalContent).toContain('deeper')
})
})
it('should navigate to /regular-route/deeper without 404 (has page)', async () => {
// Navigate directly via URL to avoid potential link click issues
const browser = await next.browser('/regular-route/deeper')
await retry(async () => {
// Since this is NOT an interception route, we should see the actual page content
// The page should render in the main content area, not in a modal
const bodyText = await browser.elementByCss('body').text()
expect(bodyText).toContain('Regular route without default.tsx')
expect(bodyText).toContain('deeper/page.tsx')
})
})
/**
* Explicit layout test: Verify behavior with layout.tsx but no parallel routes
*/
it('should navigate to /explicit-layout/deeper without 404', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
await act(async () => {
await navigate(browser, '/explicit-layout/deeper')
})
await retry(async () => {
const modalContent = await browser.elementByCss('#modal').text()
expect(modalContent).toContain('Explicit layout')
expect(modalContent).toContain('Deeper page under explicit layout')
})
})
/**
* Repeated navigation test: Validate __DEFAULT__ marker handling is consistent
* Uses act() to ensure navigation requests return 200 (not 404). Each forward
* navigation triggers an RSC request (even if cached), while back navigation
* uses browser history without network requests.
*/
it('should handle repeated interceptions without 404', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
for (let i = 0; i < 3; i++) {
const isAccordionOpen = i > 0
await expect(
isAccordionClosed(browser, '/test-nested')
).resolves.toBe(!isAccordionOpen)
// Forward navigation: triggers RSC request (validates no 404)
await act(
async () => {
await navigate(browser, '/test-nested')
},
!isAccordionOpen ? undefined : 'no-requests'
)
await retry(async () => {
const modalContent = await browser.elementByCss('#modal').text()
expect(modalContent).toContain('Intercepted test-nested sidebar')
})
// Back navigation: uses browser history, no network request
await act(async () => {
await browser.back()
}, 'no-requests')
await retry(async () => {
const url = await browser.url()
expect(url).toMatch(/\/$/)
})
}
})
/**
* Cross-interception navigation
*/
it('should navigate between different interception routes without 404', async () => {
const { act, browser } = await createBrowserWithRouterAct('/')
// First interception
await act(async () => {
await navigate(browser, '/test-nested')
})
await retry(async () => {
const modalContent = await browser.elementByCss('#modal').text()
expect(modalContent).toContain('Intercepted test-nested sidebar')
})
// Second interception
await act(async () => {
await navigate(browser, '/has-both')
})
await retry(async () => {
const modalContent = await browser.elementByCss('#modal').text()
expect(modalContent).toContain('TEST CASE 3')
})
})
})
}
})

View File

@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}
module.exports = nextConfig