next.js/test/development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts
instant-navs-devtools.test.ts224 lines7.7 KB
import { nextTestSetup } from 'e2e-utils'
import { retry, toggleDevToolsIndicatorPopover } from 'next-test-utils'
import { Playwright } from 'next-webdriver'

describe('instant-nav-panel', () => {
  const { isNextDev, isTurbopack, next } = nextTestSetup({
    files: __dirname,
  })

  async function waitForPanelRouterTransition() {
    // Run all the necessary CSS transitions
    // and click-outside event handler adjustment due to cascading update.
    // TODO: Consider disabling transitions entirely in Next.js tests.
    await new Promise((resolve) =>
      setTimeout(
        resolve,
        // MENU_DURATION_MS + some flakiness buffer
        200 + 50
      )
    )
  }

  async function waitForInstantModeCookie(browser: Playwright): Promise<void> {
    await retry(async () => {
      const cookie = await browser.eval(() => document.cookie)
      expect(cookie).toMatch(/next-instant-navigation-testing=[^;]+/)
    })
  }

  async function clearInstantModeCookie(browser: Playwright) {
    await browser.eval(() => {
      document.cookie = 'next-instant-navigation-testing=; path=/; max-age=0'
    })
  }

  async function clickInstantNavMenuItem(browser: Playwright) {
    await browser.elementByCss('[data-instant-nav]').click()
  }

  async function clickStartClientNav(browser: Playwright) {
    await browser
      // TODO: Monitor if we need to increase timeouts for all *instant calls
      .elementByCss('[data-instant-nav-client]', { timeout: 50 })
      .click()
    await waitForInstantModeCookie(browser)
  }

  async function getInstantNavPanelText(browser: Playwright): Promise<string> {
    return browser.elementByCssInstant('.instant-nav-panel').text()
  }

  async function closePanelViaHeader(browser: Playwright) {
    return browser.elementByCss('#_next-devtools-panel-close').click()
  }

  async function hasInstantNavPanelOpen(browser: Playwright): Promise<void> {
    await browser.elementByCssInstant('.instant-nav-panel')
  }

  async function openInstantNavPanel(browser: Playwright) {
    await toggleDevToolsIndicatorPopover(browser)
    await waitForPanelRouterTransition()
    await clickInstantNavMenuItem(browser)

    await retry(
      async () => {
        await hasInstantNavPanelOpen(browser)
      },
      5_000,
      500
    )
    await waitForPanelRouterTransition()
  }

  it('should open panel in waiting state without setting cookie', async () => {
    const browser = await next.browser('/')
    await clearInstantModeCookie(browser)
    await browser.waitForElementByCss('[data-testid="home-title"]')

    await openInstantNavPanel(browser)

    // Panel should show waiting state with Page load and Client navigation sections
    await retry(async () => {
      const text = await getInstantNavPanelText(browser)
      expect(text).toContain('Page load')
      expect(text).toContain('Client navigation')
    })

    // Cookie should NOT be set yet (only set when user clicks Reload or Start)
    const cookie = await browser.eval(() => document.cookie)
    expect(cookie).not.toContain('next-instant-navigation-testing=')

    // Clean up
    await clearInstantModeCookie(browser)
  })

  it('should show client nav state after clicking Start and navigating', async () => {
    const targetPage = '/target-page/my-post?search=foo'
    const [browser] = await Promise.all([
      next.browser('/'),
      isNextDev && !isTurbopack
        ? // warmup target page compilation before clicking Start, to avoid extra flakiness.
          next.render(targetPage).catch(() => {})
        : null,
    ])
    await clearInstantModeCookie(browser)
    await browser.waitForElementByCss('[data-testid="home-title"]')

    await openInstantNavPanel(browser)

    // Click Start to enter client-nav-waiting state
    await clickStartClientNav(browser)

    // Cookie should now be set
    await waitForInstantModeCookie(browser)

    // Panel should show client-nav-waiting state
    await retry(async () => {
      const text = await getInstantNavPanelText(browser)
      expect(text).toContain('Client navigation')
      expect(text).toContain('Click any link')
    })

    // Navigate to target page via SPA (use eval to bypass overlay pointer interception)
    await browser.eval((page) => {
      document.querySelector<HTMLAnchorElement>(`[href="${page}"]`)!.click()
    }, targetPage)

    // Panel should transition to client-nav state
    await retry(async () => {
      const text = await getInstantNavPanelText(browser)
      expect(text).toContain('Client navigation')
      expect(text).toContain('prefetched UI')
      expect(text).toContain('Continue rendering')
    })

    // Clean up
    await clearInstantModeCookie(browser)
  })

  it('should show loading skeletons during SPA navigation after clicking Start', async () => {
    const targetPage = '/target-page/my-post?search=foo'
    const [browser] = await Promise.all([
      next.browser('/'),
      isNextDev && !isTurbopack
        ? // warmup target page compilation before clicking Start, to avoid extra flakiness.
          next.render(targetPage).catch(() => {})
        : null,
    ])
    await clearInstantModeCookie(browser)
    await browser.waitForElementByCss('[data-testid="home-title"]')

    await openInstantNavPanel(browser)

    // Click Start to activate the navigation lock
    await clickStartClientNav(browser)

    // Navigate to target page via SPA (use eval to bypass overlay pointer interception)
    await browser.eval((page) => {
      document.querySelector<HTMLAnchorElement>(`[href="${page}"]`)!.click()
    }, targetPage)

    // Every runtime-dependent segment should be suspended under the lock:
    // data-fetching (dynamic content), `await params`, and `await searchParams`.
    // Use a longer timeout because dev mode needs to compile the target page.
    await browser
      .locator('[data-testid="dynamic-skeleton"]')
      .waitFor({ state: 'visible', timeout: 30000 })
    await browser
      .locator('[data-testid="param-skeleton"]')
      .waitFor({ state: 'visible' })
    await browser
      .locator('[data-testid="search-param-skeleton"]')
      .waitFor({ state: 'visible' })
    // The resolved param value must not have leaked through the lock.
    expect(await browser.locator('[data-testid="param-value"]').count()).toBe(0)

    // Clean up
    await clearInstantModeCookie(browser)
  })

  it('should auto-open panel on page load when cookie is already set', async () => {
    const browser = await next.browser('/')
    await clearInstantModeCookie(browser)
    await browser.waitForElementByCss('[data-testid="home-title"]')

    // Open the panel and click Start to set the cookie
    await openInstantNavPanel(browser)
    await clickStartClientNav(browser)

    // Reload — the cookie persists, so the panel should auto-open
    await browser.refresh()
    await browser.waitForElementByCss('[data-testid="home-title"]')

    await retry(async () => {
      await hasInstantNavPanelOpen(browser)
    })

    // Clean up
    await clearInstantModeCookie(browser)
  })

  it('should not set cookie when closing panel from waiting state', async () => {
    const browser = await next.browser('/')
    await clearInstantModeCookie(browser)
    await browser.waitForElementByCss('[data-testid="home-title"]')

    await openInstantNavPanel(browser)

    // Verify cookie is NOT set (panel opened without activating lock)
    const cookie = await browser.eval(() => document.cookie)
    expect(cookie).not.toContain('next-instant-navigation-testing=')

    // Close panel via X button
    await closePanelViaHeader(browser)

    // Cookie should still not be set, and no reload should happen
    await retry(async () => {
      const cookieAfter = await browser.eval(() => document.cookie)
      expect(cookieAfter).not.toContain('next-instant-navigation-testing=')
    })
  })
})
Quest for Codev2.0.0
/
SIGN IN