next.js/test/e2e/app-dir/use-offline/use-offline.test.ts
use-offline.test.ts163 lines5.3 KB
import { nextTestSetup } from 'e2e-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'
import { retry } from 'next-test-utils'

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

  if (isNextDev) {
    test('skipped in dev mode', () => {})
    return
  }

  // Uses Playwright's built-in network emulation, which fires the browser's
  // native offline/online events and blocks all requests at the network layer.
  async function goOffline(page: Playwright.Page) {
    await page.context().setOffline(true)
  }

  async function goOnline(page: Playwright.Page) {
    await page.context().setOffline(false)
  }

  it('retries navigation after connectivity is restored', async () => {
    let act: ReturnType<typeof createRouterAct>
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
        act = createRouterAct(p)
      },
    })

    // Verify we're on the home page and online
    expect(await browser.elementById('home').text()).toContain('Home')
    expect(await browser.elementById('offline-status').text()).toBe('online')

    // Prefetch the destination link
    await act(async () => {
      const toggle = await browser.elementByCss(
        'input[data-link-accordion="/destination"]'
      )
      await toggle.click()
    })

    // Go offline, then click the link. The prefetched static shell should
    // render immediately (from the cache), but the dynamic content behind
    // the Suspense boundary won't load.
    await goOffline(page!)

    const link = await browser.elementByCss('a[href="/destination"]')
    await link.click()

    // The destination page's static shell renders from the prefetch cache.
    await retry(async () => {
      expect(await browser.elementById('destination-content').text()).toContain(
        'Destination page'
      )
    })

    // The Suspense fallback is visible and shows the offline indicator.
    expect(await browser.elementById('destination-loading').text()).toContain(
      'Waiting for data...'
    )
    // useOffline() returns true in both the layout and the Suspense fallback.
    expect(await browser.elementById('offline-status').text()).toBe('offline')
    expect(await browser.elementById('destination-loading').text()).toContain(
      'offline'
    )

    // The dynamic content hasn't loaded yet.
    expect(await browser.hasElementByCssSelector('#destination-dynamic')).toBe(
      false
    )

    // Restore connectivity. The dynamic content should stream in.
    await goOnline(page!)

    await retry(async () => {
      expect(await browser.elementById('destination-dynamic').text()).toBe(
        'Dynamic data loaded'
      )
    })

    // useOffline() should return false after reconnection
    expect(await browser.elementById('offline-status').text()).toBe('online')
  })

  it('shows offline indicator even without prefetch data', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })

    expect(await browser.elementById('home').text()).toContain('Home')
    expect(await browser.elementById('offline-status').text()).toBe('online')

    // Go offline BEFORE revealing the link — no prefetch will happen.
    await goOffline(page!)

    // Reveal the link and click it. Both the prefetch and the navigation
    // fetch will fail since we're offline.
    const toggle = await browser.elementByCss(
      'input[data-link-accordion="/destination"]'
    )
    await toggle.click()
    const link = await browser.elementByCss('a[href="/destination"]')
    await link.click()

    // The optimistic update should still surface the offline state even
    // though the navigation is stuck in a pending transition with no
    // cached data at all.
    await retry(async () => {
      expect(await browser.elementById('offline-status').text()).toBe('offline')
    })
    expect(await browser.hasElementByCssSelector('#destination-content')).toBe(
      false
    )
    expect(await browser.elementById('home').text()).toContain('Home')

    // Restore connectivity. Navigation should complete.
    await goOnline(page!)

    await retry(async () => {
      expect(await browser.elementById('destination-dynamic').text()).toBe(
        'Dynamic data loaded'
      )
    })
    expect(await browser.elementById('offline-status').text()).toBe('online')
  })

  it('updates offline indicator immediately on disconnect', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })

    expect(await browser.elementById('offline-status').text()).toBe('online')

    // Disconnect without performing any navigation. The offline event
    // should flip useOffline() to true immediately.
    await goOffline(page!)

    await retry(async () => {
      expect(await browser.elementById('offline-status').text()).toBe('offline')
    })

    // Reconnect. The online event triggers a connectivity check, and
    // useOffline() should flip back to false.
    await goOnline(page!)

    await retry(async () => {
      expect(await browser.elementById('offline-status').text()).toBe('online')
    })
  })
})
Quest for Codev2.0.0
/
SIGN IN