Files
next.js/test/e2e/app-dir/interception-dynamic-single-segment/interception-dynamic-single-segment.test.ts
Wyatt JohnsonandGitHub dc61d29e49 Interception routes match from nested route navigation (#84898)
### What?

Refactors interception route regex generation to fix nested route
navigation and improve maintainability.

### Why?

Interception routes failed to trigger when navigating from nested
descendant routes. The previous implementation had overly complex,
fragile regex patterns spread across multiple functions that were
difficult to maintain and didn't handle all navigation scenarios
correctly.

The root cause was an overly restrictive header matching pattern
(`/[^/]+` - single segment only) that prevented matching navigation from
deeper nested routes like `/groups/123/settings` → `/groups/123/new`.

### How?

**Simplified Architecture:**
- Consolidated regex generation logic into `route-regex.ts` where it
properly belongs
- Removed ~230 lines of complex, duplicated regex building code from
`generate-interception-routes-rewrites.ts`
- Leveraged existing `getNamedRouteRegex()` with a new `reference`
parameter to handle parameter name mapping

**Parameter Naming Fix:**
- Introduced `nxtI` prefix for parameters adjacent to interception
markers (e.g., `(.)[id]` → `nxtIid`)
- Maintains `nxtP` prefix for regular parameters
- Ensures consistent parameter naming across source, destination, and
regex patterns

**Regex Pattern Fix:**
- Changed header matching from single-segment (`/[^/]+)?` to
multi-segment `(/.+)?` pattern
- Now correctly matches navigation from any descendant route depth

**Test Coverage:**
- Added comprehensive e2e tests for nested navigation scenarios
- Updated existing tests to reflect new parameter naming convention

Fixes #84813

NAR-445
2025-10-16 16:58:09 -06:00

303 lines
12 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
describe('interception-dynamic-single-segment', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should intercept from nested route to deeper nested route with (.) modifier', async () => {
// This test covers the bug fix for same-level (.) interception
// where navigation from a nested route (e.g., /groups/123) to a deeper route
// (e.g., /groups/123/new) should trigger the modal interception.
//
// The bug was that the regex pattern used [^/]+ which only matched single segments,
// so interception failed when the source route had multiple segments like /groups/123
const browser = await next.browser('/groups/123')
// Verify we're on the group page
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Group 123')
expect(text).toContain('New Item')
})
// Navigate from /groups/123 to /groups/123/new
// This should trigger the modal interception
await browser.elementById('new-link').click()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group 123')
})
// The children should still show the group page
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('Group 123')
})
// Refresh to verify the full page renders (not intercepted)
await browser.refresh()
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('New item for group 123')
})
})
it('should intercept from deeply nested route (4 segments) with (.) modifier', async () => {
// This test covers deeply nested routes with multiple dynamic segments
// Source: /org/acme/team/engineering (4 segments, 2 dynamic)
// Target: /org/acme/team/engineering/settings (5 segments)
//
// This ensures the regex fix (.+) handles very deep nesting correctly
const browser = await next.browser('/org/acme/team/engineering')
// Verify we're on the team page
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Team engineering in Org acme')
expect(text).toContain('Settings')
})
// Navigate from /org/acme/team/engineering to /org/acme/team/engineering/settings
// This should trigger the modal interception
await browser.elementById('settings-link').click()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain(
'Modal: Settings for Team engineering in Org acme'
)
})
// The children should still show the team page
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('Team engineering in Org acme')
})
// Refresh to verify the full page renders (not intercepted)
await browser.refresh()
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain(
'Settings for Team engineering in Org acme'
)
})
})
it('should intercept with programmatic navigation using router.push', async () => {
// Test that interception works with programmatic navigation, not just Link clicks
// This ensures the NEXT_URL header is set correctly in all navigation scenarios
const browser = await next.browser('/groups/123')
// Verify we're on the group page
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Group 123')
})
// Use router.push to navigate programmatically
await browser.eval('window.next.router.push("/groups/123/new")')
// Should trigger the modal interception
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group')
})
// The children should still show the group page
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('Group 123')
})
})
it('should intercept from nested route with query parameters', async () => {
// Test that interception works when the source route has query parameters
// The query params should not interfere with route matching
const browser = await next.browser('/groups/123?tab=settings&view=grid')
// Verify we're on the group page with query params
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Group 123')
expect(text).toContain('New Item')
})
// Navigate to /groups/123/new (query params in source shouldn't affect interception)
await browser.elementById('new-link').click()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group')
})
// The children should still show the group page
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('Group 123')
})
})
it('should intercept with consecutive dynamic segments', async () => {
// Test that interception works with consecutive dynamic segments [a]/[b]/[c]
// This is an edge case where there are no static segments between dynamics
// Source: /x/y/z (3 consecutive dynamic segments)
// Target: /x/y/z/item
const browser = await next.browser('/x/y/z')
// Verify we're on the consecutive dynamic page
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Path: x/y/z')
expect(text).toContain('View Item')
})
// Navigate to /x/y/z/item
await browser.elementById('item-link').click()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: Item for path x/y/z')
})
// The children should still show the consecutive page
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('Path: x/y/z')
})
// Refresh to verify the full page renders (not intercepted)
await browser.refresh()
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('Item for path: x/y/z')
})
})
it('should intercept with purely static multi-segment paths', async () => {
// Test that interception works with static (non-dynamic) multi-segment paths
// This ensures the fix doesn't break static route interception
// Source: /admin/dashboard/users (3 static segments)
// Target: /admin/dashboard/users/new (4 static segments)
const browser = await next.browser('/admin/dashboard/users')
// Verify we're on the users page
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Admin Dashboard - Users')
expect(text).toContain('New User')
})
// Navigate to /admin/dashboard/users/new
await browser.elementById('new-user-link').click()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: New User Form')
})
// The children should still show the users page
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('Admin Dashboard - Users')
})
// Refresh to verify the full page renders (not intercepted)
await browser.refresh()
await retry(async () => {
const childrenText = await browser.elementById('children').text()
expect(childrenText).toContain('New User Form')
})
})
describe('nested navigation - descendants of intercepting route', () => {
// These tests verify the key bug fix: the regex pattern now matches
// all descendants of the intercepting route level, not just the exact level.
// Previously, navigation FROM a nested route (e.g., /groups/123/nested) would
// fail to trigger interception. Now it should work from any depth.
// NOTE: These are conceptual tests - actual nested pages would need to be created
// in the app directory structure to fully test this behavior in a real app.
it('should intercept when navigating from a child route using back navigation', async () => {
// Start at /groups/123, navigate to /groups/123/new (intercepted),
// then navigate away and back
const browser = await next.browser('/groups/123')
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Group 123')
})
// First navigation - should intercept
await browser.elementById('new-link').click()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group')
})
// Navigate back
await browser.back()
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Group 123')
})
// Navigate forward again - should still intercept
await browser.forward()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group')
})
})
it('should intercept multiple times from the same route', async () => {
// Test that interception works consistently on repeated navigation
const browser = await next.browser('/groups/456')
for (let i = 0; i < 3; i++) {
await browser.elementById('new-link').click()
await retry(async () => {
const modalText = await browser.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group 456')
})
// Go back to test again
await browser.back()
await retry(async () => {
const text = await browser.elementByCss('body').text()
expect(text).toContain('Group 456')
})
}
})
it('should intercept when navigating between different dynamic segments', async () => {
// Test interception works across different dynamic route values
// First group
const browser1 = await next.browser('/groups/100')
await browser1.elementById('new-link').click()
await retry(async () => {
const modalText = await browser1.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group 100')
})
// Second group - new browser instance to test fresh navigation
const browser2 = await next.browser('/groups/200')
await retry(async () => {
const text = await browser2.elementByCss('body').text()
expect(text).toContain('Group 200')
})
// Intercept from second group - should still work
await browser2.elementById('new-link').click()
await retry(async () => {
const modalText = await browser2.elementById('modal').text()
expect(modalText).toContain('Modal: New item for group 200')
})
})
})
})