Files
textbee/web/lib/httpBrowserClient.test.ts
isra el 18e7d1b3d8 perf(web): make dashboard navigation fast and cut session fetches
- prefetch route-tab links so tab clicks reuse a cached payload instead
  of paying an uncached server round trip each time
- add loading.tsx boundaries (dashboard root and each section) so clicks
  paint within a frame; section-level files keep the header and tabs
  mounted while only the content area swaps
- link the sidebar Account item and the checkout page straight to
  /dashboard/account/billing. The /dashboard/account redirect stub stays
  for old links, but no internal link pays the extra hop anymore. The
  stub also dropped query params, which silently ate the plan-change
  success toast.
- set QueryClient defaults (staleTime 60s, no focus refetch, retry 1);
  mutations already invalidate their keys, so the user's own changes
  stay instant. Device messages and webhook deliveries get a 15s
  staleTime since they change from outside the tab.
- replace the axios getCachedSession TTL cache with a token seeded from
  the server session in Providers and kept in sync by a session bridge.
  Requests attach the token synchronously; /api/auth/session is only a
  deduped fallback, instead of a refetch every 2 minutes with a
  thundering herd on expiry.
- swap the billing card's 16px loading spinner for a card-shaped
  skeleton so the tab no longer looks blank while loading

Tests: interceptor seeding/dedupe/signed-out behavior, provider
defaults and token seeding, nav active-state matching, tab prefetch,
billing loading state, plus e2e coverage for direct-to-billing
navigation and an at-most-one-session-call budget guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:49:14 +03:00

84 lines
2.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { http, HttpResponse } from 'msw'
import { server } from '@/test/msw/server'
import { API_BASE_URL } from '@/test/fixtures'
// Own getSession mock (overriding the global one in test/setup.ts) so calls
// can be counted and resolved per test.
const getSessionMock = vi.hoisted(() => vi.fn())
vi.mock('next-auth/react', async (importOriginal) => {
const actual = await importOriginal<typeof import('next-auth/react')>()
return { ...actual, getSession: getSessionMock }
})
// The token lives in module state, so each test loads a fresh module copy.
async function loadClient() {
vi.resetModules()
return import('@/lib/httpBrowserClient')
}
function captureAuthHeaders() {
const headers: (string | null)[] = []
server.use(
http.get(`${API_BASE_URL}/ping`, ({ request }) => {
headers.push(request.headers.get('authorization'))
return HttpResponse.json({ ok: true })
})
)
return headers
}
afterEach(() => {
getSessionMock.mockReset()
})
describe('httpBrowserClient auth interceptor', () => {
it('attaches a seeded token without calling getSession', async () => {
const { default: client, setSessionToken } = await loadClient()
setSessionToken('abc')
const headers = captureAuthHeaders()
await client.get('/ping')
expect(headers).toEqual(['Bearer abc'])
expect(getSessionMock).not.toHaveBeenCalled()
})
it('dedupes concurrent session fetches when the token is not seeded', async () => {
getSessionMock.mockResolvedValue({ user: { accessToken: 'tok1' } })
const { default: client } = await loadClient()
const headers = captureAuthHeaders()
await Promise.all([client.get('/ping'), client.get('/ping')])
expect(getSessionMock).toHaveBeenCalledTimes(1)
expect(headers).toEqual(['Bearer tok1', 'Bearer tok1'])
})
it('sends no header and no session fetch when seeded signed out', async () => {
const { default: client, setSessionToken } = await loadClient()
// null means known signed out (the 401 handler and the auth pages), so
// requests must not fall back to a /api/auth/session round trip.
setSessionToken(null)
const headers = captureAuthHeaders()
await client.get('/ping')
expect(headers).toEqual([null])
expect(getSessionMock).not.toHaveBeenCalled()
})
it('recovers with a fresh fetch after a failed session lookup', async () => {
getSessionMock.mockRejectedValueOnce(new Error('network down'))
getSessionMock.mockResolvedValueOnce({ user: { accessToken: 'tok2' } })
const { default: client } = await loadClient()
const headers = captureAuthHeaders()
await expect(client.get('/ping')).rejects.toThrow('network down')
await client.get('/ping')
expect(getSessionMock).toHaveBeenCalledTimes(2)
expect(headers).toEqual(['Bearer tok2'])
})
})