hypertwist/website/src/__tests__/route-shells.test.tsx
2026-07-03 03:32:15 +00:00

265 lines
8.8 KiB
TypeScript

import { cleanup, render, screen } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import userEvent from '@testing-library/user-event'
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ROUTER_FUTURE_FLAGS } from '../router/router-future'
const mockUsePlatformAuth = vi.fn()
const mockLogout = vi.fn()
const mockToggleColorMode = vi.fn()
const mockGetAuthHealth = vi.fn()
const mockGetReleaseManifest = vi.fn()
vi.mock('../auth/platform-auth', () => ({
usePlatformAuth: () => mockUsePlatformAuth(),
}))
vi.mock('../auth/auth-api', () => ({
getAuthHealth: (...args: unknown[]) => mockGetAuthHealth(...args),
getReleaseManifest: (...args: unknown[]) => mockGetReleaseManifest(...args),
}))
import { MarketingShell } from '../components/layout/MarketingShell'
import { AppShell } from '../components/layout/AppShell'
import { ProtectedRoute } from '../components/routes/ProtectedRoute'
function LocationEcho() {
const location = useLocation()
return <div data-testid="location">{location.pathname}{location.search}{location.hash}</div>
}
function renderProtectedRoute(initialEntry: string) {
return render(
<MemoryRouter initialEntries={[initialEntry]} future={ROUTER_FUTURE_FLAGS}>
<Routes>
<Route element={<ProtectedRoute />}>
<Route path="/app/account" element={<div>Account page</div>} />
</Route>
<Route path="/login" element={<LocationEcho />} />
</Routes>
</MemoryRouter>,
)
}
function renderMarketingShell(isAuthenticated = false) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
mockUsePlatformAuth.mockReturnValue({
isAuthenticated,
isLoading: false,
logout: () => mockLogout(),
toggleColorMode: () => mockToggleColorMode(),
colorMode: 'dark',
user: {
name: 'Operator',
plan: 'operator',
},
})
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter future={ROUTER_FUTURE_FLAGS}>
<MarketingShell eyebrow="Eyebrow" title="Title" lede="Lede">
<div>Body</div>
</MarketingShell>
</MemoryRouter>
</QueryClientProvider>,
)
}
describe('website route and shell behavior', () => {
beforeEach(() => {
cleanup()
mockUsePlatformAuth.mockReset()
mockLogout.mockReset()
mockToggleColorMode.mockReset()
mockGetAuthHealth.mockReset()
mockGetReleaseManifest.mockReset()
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: false,
isLoading: false,
logout: () => mockLogout(),
toggleColorMode: () => mockToggleColorMode(),
colorMode: 'dark',
user: {
name: 'Operator',
plan: 'operator',
},
})
mockGetAuthHealth.mockResolvedValue({
ok: true,
service: 'hypertwist-auth-server',
supertokens: {
configured: true,
reachable: true,
ready: true,
apiVersion: '5.4',
error: null,
oauth: {
github: false,
google: false,
},
},
fallback: {
enabled: true,
active: false,
reason: null,
},
billing: {
statePath: '/var/lib/hypertwist/auth/hypertwist-billing-state.json',
processedEventCount: 0,
pricePlanMapConfigured: false,
productPlanMapConfigured: false,
webhookSecretConfigured: false,
},
runtime: {
mode: 'mixed',
public_origin_ready: true,
cookie_secure: true,
api_domain: 'https://hypertwist.app',
website_domain: 'https://hypertwist.app',
warnings: [],
errors: [],
},
})
mockGetReleaseManifest.mockResolvedValue({
ok: true,
manifest: {
generated_at: '2026-06-22T12:00:00.000Z',
support_email: 'hello@hypertwist.app',
public_docs_url: 'https://docs.hypertwist.app',
release_notes_url: 'https://notes.hypertwist.app',
corresponding_source_url: 'https://hypertwist.app/open-source/source.zip',
open_source_repo_url: 'https://github.com/hypertwist/hypertwist',
viewer: {
authenticated: false,
canDownload: false,
plan: null,
role: null,
accessStatus: null,
},
platforms: [],
},
})
})
it('shows the protected-route loader while auth state is still resolving', () => {
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: false,
isLoading: true,
})
const { container } = renderProtectedRoute('/app/account')
expect(screen.getByText('Loading operator access...')).toBeTruthy()
expect(screen.getByText('Browser account access')).toBeTruthy()
expect(screen.getByText(/Checking your saved browser session, protected release status, and browser-to-desktop continuity/i)).toBeTruthy()
expect(container.querySelector('.general-page-loader-shell--fullscreen')).not.toBeNull()
expect(screen.getByAltText('HyperTwist')).toBeTruthy()
expect(screen.getByRole('link', { name: 'Open sign-in' }).getAttribute('href')).toBe('/login?next=%2Fapp%2Faccount')
expect(screen.getByRole('link', { name: 'Open support' }).getAttribute('href')).toBe('/support?topic=operator-access')
})
it('redirects unauthenticated protected routes to login with the full encoded next target', async () => {
renderProtectedRoute('/app/account?tab=billing#security')
expect((await screen.findByTestId('location')).textContent).toBe('/login?next=%2Fapp%2Faccount%3Ftab%3Dbilling%23security')
})
it('renders the protected outlet when authentication is present', () => {
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: true,
isLoading: false,
})
renderProtectedRoute('/app/account')
expect(screen.getByText('Account page')).toBeTruthy()
})
it('switches the marketing-shell auth action between login and dashboard', () => {
const { rerender } = renderMarketingShell(false)
expect(screen.getByRole('link', { name: 'Log in' }).getAttribute('href')).toBe('/login')
expect(screen.getAllByRole('link', { name: 'Features' }).every((link) => link.getAttribute('href') === '/features')).toBe(true)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: true,
isLoading: false,
logout: () => mockLogout(),
toggleColorMode: () => mockToggleColorMode(),
colorMode: 'dark',
user: {
name: 'Operator',
plan: 'operator',
},
})
rerender(
<QueryClientProvider client={queryClient}>
<MemoryRouter future={ROUTER_FUTURE_FLAGS}>
<MarketingShell eyebrow="Eyebrow" title="Title" lede="Lede">
<div>Body</div>
</MarketingShell>
</MemoryRouter>
</QueryClientProvider>,
)
expect(screen.getByRole('link', { name: 'Open dashboard' }).getAttribute('href')).toBe('/app')
})
it('shows operator identity in the app shell and wires theme/logout actions', async () => {
mockUsePlatformAuth.mockReturnValue({
isAuthenticated: true,
isLoading: false,
logout: () => mockLogout(),
toggleColorMode: () => mockToggleColorMode(),
colorMode: 'dark',
user: {
name: 'Operator Prime',
plan: 'studio',
},
})
render(
<MemoryRouter initialEntries={['/app']} future={ROUTER_FUTURE_FLAGS}>
<Routes>
<Route path="/app" element={<AppShell />}>
<Route index element={<div>Overview content</div>} />
</Route>
</Routes>
</MemoryRouter>,
)
expect(screen.getByText('Operator Prime')).toBeTruthy()
expect(screen.getByText('studio plan')).toBeTruthy()
expect(screen.getByText('Overview content')).toBeTruthy()
expect(screen.getByText('Desktop-first operator lane')).toBeTruthy()
expect(screen.getByRole('link', { name: 'Launch Status' }).getAttribute('href')).toBe('/app/launch-status')
expect(screen.getByRole('link', { name: 'Protected launch status' }).getAttribute('href')).toBe('/app/launch-status')
expect(screen.getByRole('link', { name: 'Public download guidance' }).getAttribute('href')).toBe('/download')
expect(screen.getByRole('link', { name: 'Public notices' }).getAttribute('href')).toBe('/open-source-notices')
expect(screen.getByRole('link', { name: 'Operator access support' }).getAttribute('href')).toBe('/support?topic=operator-access')
await userEvent.click(screen.getByRole('button', { name: 'Switch to light mode' }))
await userEvent.click(screen.getByRole('button', { name: 'Log out' }))
expect(mockToggleColorMode).toHaveBeenCalledTimes(1)
expect(mockLogout).toHaveBeenCalledTimes(1)
})
})