next.js/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts
segment-cache-stale-time.test.ts285 lines8.4 KB
import { nextTestSetup } from 'e2e-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'

describe('segment cache (staleness)', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    test('disabled in development / deployment', () => {})
    return
  }

  it('entry expires when its stale time has elapsed', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRouterAct(page)

    await page.clock.install()

    // Reveal the link to trigger a prefetch
    const toggle2MinutesLink = await browser.elementByCss(
      'input[data-link-accordion="/stale-2-minutes"]'
    )
    const toggle4MinutesLink = await browser.elementByCss(
      'input[data-link-accordion="/stale-4-minutes"]'
    )
    await act(
      async () => {
        await toggle2MinutesLink.click()
        await browser.elementByCss('a[href="/stale-2-minutes"]')
      },
      {
        includes: 'Content with stale time of 2 minutes',
      }
    )
    await act(
      async () => {
        await toggle4MinutesLink.click()
        await browser.elementByCss('a[href="/stale-4-minutes"]')
      },
      {
        includes: 'Content with stale time of 4 minutes',
      }
    )

    // Hide the links
    await toggle2MinutesLink.click()
    await toggle4MinutesLink.click()

    // Fast forward 2 minutes and 1 millisecond
    await page.clock.fastForward(2 * 60 * 1000 + 1)

    // Reveal the links again to trigger new prefetch tasks
    await act(
      async () => {
        await toggle2MinutesLink.click()
        await browser.elementByCss('a[href="/stale-2-minutes"]')
      },
      // The page with a stale time of 2 minutes is requested again
      // because its stale time elapsed.
      {
        includes: 'Content with stale time of 2 minutes',
      }
    )

    await act(
      async () => {
        await toggle4MinutesLink.click()
        await browser.elementByCss('a[href="/stale-4-minutes"]')
      },
      // The page with a stale time of 4 minutes is *not* requested again
      // because it's still fresh.
      'no-requests'
    )
  })

  it('expires runtime prefetches when their stale time has elapsed', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRouterAct(page)

    await page.clock.install()

    // Reveal the links to trigger a runtime prefetch
    const toggle2MinutesLink = await browser.elementByCss(
      'input[data-link-accordion="/runtime-stale-2-minutes"]'
    )
    const toggle4MinutesLink = await browser.elementByCss(
      'input[data-link-accordion="/runtime-stale-4-minutes"]'
    )
    await act(
      async () => {
        await toggle2MinutesLink.click()
        await browser.elementByCss('a[href="/runtime-stale-2-minutes"]')
      },
      {
        includes: 'Content with stale time of 2 minutes',
      }
    )
    await act(
      async () => {
        await toggle4MinutesLink.click()
        await browser.elementByCss('a[href="/runtime-stale-4-minutes"]')
      },
      {
        includes: 'Content with stale time of 4 minutes',
      }
    )

    // Hide the links
    await toggle2MinutesLink.click()
    await toggle4MinutesLink.click()

    // Fast forward 2 minutes and 1 millisecond
    await page.clock.fastForward(2 * 60 * 1000 + 1)

    // Reveal the links again to trigger new prefetch tasks
    await act(
      async () => {
        await toggle2MinutesLink.click()
        await browser.elementByCss('a[href="/runtime-stale-2-minutes"]')
      },
      // The page with a stale time of 2 minutes is requested again
      // because its stale time elapsed.
      {
        includes: 'Content with stale time of 2 minutes',
      }
    )

    await act(
      async () => {
        await toggle4MinutesLink.click()
        await browser.elementByCss('a[href="/runtime-stale-4-minutes"]')
      },
      // The page with a stale time of 4 minutes is *not* requested again
      // because it's still fresh.
      'no-requests'
    )
  })

  it('reuses dynamic data up to the staleTimes.dynamic threshold', async () => {
    let page: Playwright.Page
    const startDate = Date.now()

    const browser = await next.browser('/', {
      async beforePageLoad(p: Playwright.Page) {
        page = p
        await page.clock.install()
        await page.clock.setFixedTime(startDate)
      },
    })

    const act = createRouterAct(page)

    // Navigate to the dynamic page
    await act(
      async () => {
        const toggle = await browser.elementByCss(
          'input[data-link-accordion="/dynamic"]'
        )
        await toggle.click()
        const link = await browser.elementByCss('a[href="/dynamic"]')
        await link.click()
      },
      {
        includes: 'Dynamic content',
      }
    )
    expect(await browser.elementById('dynamic-content').text()).toBe(
      'Dynamic content'
    )

    await browser.back()

    // Advance time by 29 seconds. staleTimes.dynamic is configured as 30s, so
    // if we navigate to the same link again, the old data should be reused
    // without a new network request.
    await page.clock.setFixedTime(startDate + 29 * 1000)

    await act(async () => {
      const link = await browser.elementByCss('a[href="/dynamic"]')
      await link.click()
      // The next page is immediately rendered
      expect(await browser.elementById('dynamic-content').text()).toBe(
        'Dynamic content'
      )
    }, 'no-requests')

    await browser.back()

    // Advance an additional second. This time, if we navigate to the link
    // again, the data is stale, so we issue a new request.
    await page.clock.setFixedTime(startDate + 30 * 1000)

    await act(
      async () => {
        const link = await browser.elementByCss('a[href="/dynamic"]')
        await link.click()
      },
      { includes: 'Dynamic content' }
    )
    expect(await browser.elementById('dynamic-content').text()).toBe(
      'Dynamic content'
    )
  })

  it('caches omitted from the prerender should not affect when the prefetch is expired', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRouterAct(page)

    await page.clock.install()

    // Reveal the link to trigger a prefetch
    await act(
      async () => {
        await browser
          .elementByCss('input[data-link-accordion="/seconds"]')
          .click()
        await browser.elementByCss('a[href="/seconds"]')
      },
      {
        // cacheLife("seconds") should be excluded from a static prerender
        includes: 'Short-lived cached content',
        block: 'reject',
      }
    )

    // Hide the link
    await browser.elementByCss('input[data-link-accordion="/seconds"]').click()

    // Fast forward 30 seconds and 1 millisecond
    // (matching the staleness of the "seconds" profile)
    const timeStep = 30 * 1000 + 1
    await page.clock.fastForward(timeStep)

    // Reveal the link again to trigger new prefetch tasks.
    // The cache with `cacheLife('seconds'`) should not affect the stale time of the prefetch,
    // because we omit it from the prerender, so we shouldn't refetch anything yet.
    await act(async () => {
      await browser
        .elementByCss('input[data-link-accordion="/seconds"]')
        .click()
      await browser.elementByCss('a[href="/seconds"]')
    }, 'no-requests')

    // Hide the link again
    await browser.elementByCss('input[data-link-accordion="/seconds"]').click()

    // Fast forward to 5 minutes and 1 millisecond after the prefetch.
    // (matching the staleness of the "minutes" profile)
    // Note that we should exclude the timestep we've already done.
    await page.clock.fastForward(5 * 60 * 1000 + 1 - timeStep)

    // Reveal the link to trigger a prefetch.
    // The longer-lived cache we used on the page should make the previous prefetch expire,
    // so we should issue a new request.
    await act(
      async () => {
        await browser
          .elementByCss('input[data-link-accordion="/seconds"]')
          .click()
        await browser.elementByCss('a[href="/seconds"]')
      },
      {
        includes: 'Short-lived cached content',
        block: 'reject',
      }
    )
  })
})
Quest for Codev2.0.0
/
SIGN IN