next.js/test/e2e/app-dir/static-siblings/static-siblings.test.ts
static-siblings.test.ts231 lines9.0 KB
/**
 * Static Siblings Tests
 *
 * When a dynamic route like /products/[id] exists alongside a static route
 * like /products/sale at the same URL level, these are called "static
 * siblings." The static route should always take precedence when navigating
 * to its exact URL.
 *
 * Test approach:
 * 1. RSC payload tests (prod only): Verify that information about static
 *    siblings is included in the server response. Skipped in dev because
 *    webpack compiles routes on-demand and only knows about visited routes.
 *    (Turbopack doesn't have this limitation since it builds the full
 *    directory tree from the file system.)
 *    TODO: Replace with end-to-end tests once client behavior is implemented.
 *
 * 2. Navigation tests (dev and prod): Navigate to the dynamic route first,
 *    then go back and click the static sibling link (with prefetch={false}).
 *    Verify the static page renders correctly.
 *
 * The navigation flow ensures the client has seen the dynamic route before
 * attempting to navigate to the sibling. This simulates real-world usage
 * where a user might visit or prefetch a dynamic route, then later navigate
 * to a static sibling URL.
 */

import { nextTestSetup } from 'e2e-utils'
// TODO: These imports are only needed for the temporary RSC payload tests.
// Remove once client behavior is implemented.
import {
  NEXT_RSC_UNION_QUERY,
  RSC_HEADER,
} from 'next/dist/client/components/app-router-headers'

describe('static-siblings', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  // TODO: This helper is only needed for the temporary RSC payload tests.
  // Remove once client behavior is implemented.
  async function fetchRscResponse(url: string): Promise<string> {
    const response = await next.fetch(`${url}?${NEXT_RSC_UNION_QUERY}`, {
      headers: {
        [RSC_HEADER]: '1',
      },
    })
    return response.text()
  }

  // RSC payload tests are skipped in dev because with webpack, routes are
  // compiled on-demand, so sibling information may not be available until all
  // routes have been visited. (Turbopack doesn't have this limitation.)
  describe('cross-route-group siblings', () => {
    // TODO: Replace with end-to-end test once client behavior is implemented
    if (!isNextDev) {
      it('should include static sibling info in the server response', async () => {
        // The static sibling 'sale' is in a different route group than [id]
        const rscPayload = await fetchRscResponse('/products/123')
        expect(rscPayload).toContain('"sale"')
      })
    }

    it('should navigate to static sibling after visiting dynamic route', async () => {
      const browser = await next.browser('/')

      // Step 1: Navigate to the dynamic route first to "discover" it
      const accordion = await browser.elementByCss(
        'input[data-link-accordion="/products/123"]'
      )
      await accordion.click()
      const dynamicLink = await browser.elementByCss('a[href="/products/123"]')
      await dynamicLink.click()

      // Verify we're on the dynamic route
      const dynamicText = await browser.elementByCss('#product-page').text()
      expect(dynamicText).toBe('Product page (dynamic route)')

      // Step 2: Navigate back to the home page
      await browser.back()
      await browser.elementByCss('#home-page')

      // Step 3: Navigate to the static sibling with prefetch={false}
      const staticLink = await browser.elementByCss('#link-to-sale')
      await staticLink.click()

      // Verify the static sibling page rendered
      const staticText = await browser.elementByCss('#sale-page').text()
      expect(staticText).toBe('Sale page (static sibling)')
    })
  })

  describe('same-directory siblings', () => {
    // TODO: Replace with end-to-end test once client behavior is implemented
    if (!isNextDev) {
      it('should include static sibling info in the server response', async () => {
        // The static sibling 'featured' is in the same directory as [id]
        const rscPayload = await fetchRscResponse('/items/123')
        expect(rscPayload).toContain('"featured"')
      })
    }

    it('should navigate to static sibling after visiting dynamic route', async () => {
      const browser = await next.browser('/')

      // Step 1: Navigate to the dynamic route first to "discover" it
      const accordion = await browser.elementByCss(
        'input[data-link-accordion="/items/456"]'
      )
      await accordion.click()
      const dynamicLink = await browser.elementByCss('a[href="/items/456"]')
      await dynamicLink.click()

      // Verify we're on the dynamic route
      const dynamicText = await browser.elementByCss('#item-page').text()
      expect(dynamicText).toBe('Item page (dynamic route)')

      // Step 2: Navigate back to the home page
      await browser.back()
      await browser.elementByCss('#home-page')

      // Step 3: Navigate to the static sibling with prefetch={false}
      const staticLink = await browser.elementByCss('#link-to-featured')
      await staticLink.click()

      // Verify the static sibling page rendered
      const staticText = await browser.elementByCss('#featured-page').text()
      expect(staticText).toBe('Featured items (static sibling)')
    })
  })

  describe('parallel route siblings', () => {
    // TODO: Replace with end-to-end test once client behavior is implemented
    if (!isNextDev) {
      it('should include static sibling info in the server response', async () => {
        // The static sibling 'settings' is in a parallel route slot
        const rscPayload = await fetchRscResponse('/dashboard/123')
        expect(rscPayload).toContain('"settings"')
      })
    }

    it('should navigate to static sibling after visiting dynamic route', async () => {
      const browser = await next.browser('/')

      // Step 1: Navigate to the dynamic route first to "discover" it
      const accordion = await browser.elementByCss(
        'input[data-link-accordion="/dashboard/789"]'
      )
      await accordion.click()
      const dynamicLink = await browser.elementByCss('a[href="/dashboard/789"]')
      await dynamicLink.click()

      // Verify we're on the dynamic route
      const dynamicText = await browser.elementByCss('#panel-item-page').text()
      expect(dynamicText).toBe('Panel item (dynamic in parallel route)')

      // Step 2: Navigate back to the home page
      await browser.back()
      await browser.elementByCss('#home-page')

      // Step 3: Navigate to the static sibling with prefetch={false}
      const staticLink = await browser.elementByCss('#link-to-settings')
      await staticLink.click()

      // Verify the static sibling page rendered
      const staticText = await browser
        .elementByCss('#panel-settings-page')
        .text()
      expect(staticText).toBe(
        'Panel settings (static sibling in parallel route)'
      )
    })
  })

  describe('deeply nested siblings', () => {
    // TODO: Replace with end-to-end test once client behavior is implemented
    if (!isNextDev) {
      it('should include static sibling info in the server response', async () => {
        // The static sibling 'electronics' is deeply nested with multiple layouts
        const rscPayload = await fetchRscResponse('/categories/phones')
        expect(rscPayload).toContain('"electronics"')
        // Nested segments inside 'electronics' should NOT be leaked as siblings
        expect(rscPayload).not.toContain('"computers"')
        expect(rscPayload).not.toContain('"laptops"')
      })
    }

    it('should navigate to static sibling after visiting dynamic route', async () => {
      const browser = await next.browser('/')

      // Step 1: Navigate to the dynamic route first to "discover" it
      const accordion = await browser.elementByCss(
        'input[data-link-accordion="/categories/phones"]'
      )
      await accordion.click()
      const dynamicLink = await browser.elementByCss(
        'a[href="/categories/phones"]'
      )
      await dynamicLink.click()

      // Verify we're on the dynamic route
      const dynamicText = await browser.elementByCss('#category-page').text()
      expect(dynamicText).toContain('Dynamic category page')

      // Step 2: Navigate back to the home page
      await browser.back()
      await browser.elementByCss('#home-page')

      // Step 3: Navigate to the deeply nested static sibling with prefetch={false}
      const staticLink = await browser.elementByCss('#link-to-laptops')
      await staticLink.click()

      // Verify the static sibling page rendered with all its layouts
      const staticText = await browser.elementByCss('#laptops-page').text()
      expect(staticText).toContain('Laptops')

      // Verify the nested layouts are present
      const electronicsLayout = await browser.elementByCss(
        '[data-electronics-layout]'
      )
      expect(electronicsLayout).toBeTruthy()
      const computersLayout = await browser.elementByCss(
        '[data-computers-layout]'
      )
      expect(computersLayout).toBeTruthy()
      const laptopsLayout = await browser.elementByCss('[data-laptops-layout]')
      expect(laptopsLayout).toBeTruthy()
    })
  })
})
Quest for Codev2.0.0
/
SIGN IN