import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ZonePageClient } from '@/app/(main)/websites/[zoneId]/page-client'; import { ProxyRouteService, TlsCertificateService, ZoneService, } from '@/lib/services/openflare'; class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} } vi.stubGlobal('ResizeObserver', ResizeObserverMock); let mockZoneId = '42'; let mockParamZoneId = '42'; const replaceMock = vi.fn(); vi.mock('next/link', () => ({ default: ({ children, href, }: { children: React.ReactNode; href: string; }) => {children}, })); vi.mock('next/navigation', () => ({ useRouter: () => ({ replace: replaceMock, back: vi.fn() }), usePathname: () => `/websites/${mockZoneId}`, useSearchParams: () => new URLSearchParams(), useParams: () => ({ zoneId: mockParamZoneId }), })); vi.mock('@/lib/services/openflare', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, ZoneService: { getOverview: vi.fn(), getStats: vi.fn(), deleteById: vi.fn(), list: vi.fn(), }, TlsCertificateService: { list: vi.fn(), }, ProxyRouteService: { list: vi.fn(), }, }; }); function renderPage(zoneId: number, paramZoneId = zoneId) { mockZoneId = String(zoneId); mockParamZoneId = String(paramZoneId); const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, }, }); return render( , ); } describe('ZonePageClient', () => { beforeEach(() => { mockParamZoneId = mockZoneId; vi.mocked(ZoneService.getOverview).mockReset(); vi.mocked(ZoneService.getStats).mockReset(); vi.mocked(ZoneService.getStats).mockResolvedValue({ range: '24h', range_hours: 24, window_started_at: new Date().toISOString(), window_ended_at: new Date().toISOString(), bucket_minutes: 60, unique_visitors: 0, request_count: 0, bytes_sent: 0, domain_count: 0, available: true, series: [], }); vi.mocked(ZoneService.deleteById).mockReset(); vi.mocked(TlsCertificateService.list).mockReset(); vi.mocked(TlsCertificateService.list).mockResolvedValue([]); vi.mocked(ProxyRouteService.list).mockReset(); vi.mocked(ProxyRouteService.list).mockResolvedValue([]); }); it('loads the overview by stable ID and exposes all tabs including empty domains', async () => { vi.mocked(ZoneService.getOverview).mockImplementation(async () => ({ zone: { id: 42, domain: 'example.com', created_at: '', updated_at: '' }, domains: [], })); renderPage(42); await waitFor(() => { expect(ZoneService.getOverview).toHaveBeenCalledWith(42); }); expect( await screen.findByRole('heading', { name: 'example.com' }), ).toBeVisible(); expect(await screen.findByText('唯一访问者')).toBeVisible(); expect(screen.getByText('请求总数')).toBeVisible(); expect(screen.getByText('已提供的数据总计')).toBeVisible(); expect(screen.getByRole('tab', { name: '域名 (0)' })).toBeVisible(); expect(screen.getByRole('tab', { name: '证书 (0)' })).toBeVisible(); expect(screen.queryByRole('tab', { name: '路由' })).not.toBeInTheDocument(); expect(screen.getByRole('tab', { name: '设置' })).toBeVisible(); }); it('uses the browser pathname ID when serving a static-export fallback shell', async () => { vi.mocked(ZoneService.getOverview).mockImplementation(async () => ({ zone: { id: 42, domain: 'example.com', created_at: '', updated_at: '' }, domains: [], })); renderPage(42, 1); await waitFor(() => { expect(ZoneService.getOverview).toHaveBeenCalledWith(42); }); expect(ZoneService.getOverview).not.toHaveBeenCalledWith(1); }); it('renders a not-found state for a missing Zone', async () => { vi.mocked(ZoneService.getOverview).mockRejectedValue( new Error('Zone 不存在'), ); renderPage(42); expect( await screen.findByText('网站不存在,可能已被删除或 ID 无效。'), ).toBeVisible(); }); it('renders invalid ID empty state without calling the API', async () => { renderPage(0); expect( await screen.findByText('无效的网站 ID,请从网站列表进入详情页。'), ).toBeVisible(); expect(ZoneService.getOverview).not.toHaveBeenCalled(); }); });