import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; import AnalyticsPage from './views/AnalyticsPage'; import { I18nProvider } from './i18n/I18nProvider'; import { api } from './api'; import { getUserKeyFromToken } from './themePrefs'; const mockedApi = api as jest.Mocked; const snapshot = { total: 5, active: 4, byStatus: { Applied: 2, 'Faglig vurdering': 2 } }; const overview = { funnel: [{ label: 'Applied', count: 2 }], responseRateBySource: [{ label: 'NAV', total: 4, responses: 1, rate: 25 }], topCompanies: [{ companyId: 7, company: 'Nordlys æøå', count: 4, responses: 1, responseRate: 25 }], timeInStage: [{ stage: 'Applied', medianDays: 3, count: 2 }], salaryInsights: [{ currency: 'NOK', period: 'year', count: 2, minimum: 600000, maximum: 800000, averageMidpoint: 700000 }] }; function response(url: string) { if (url.endsWith('/stats')) return snapshot; if (url.endsWith('/analytics-overview')) return overview; if (url.endsWith('/analytics')) return [{ month: '2026-08', applied: 2, responses: 1 }, { month: '2026-09', applied: 1, responses: 0 }]; if (url.endsWith('/tags')) return Array.from({ length: 10 }, (_, i) => ({ tag: `Tag ${i}`, count: i + 1 })); if (url.endsWith('/tag-trends')) return { months: ['2026-08', '2026-09'], series: [{ tag: 'Azure', counts: [2, 1] }] }; return []; } function History() { const location = useLocation(); const navigate = useNavigate(); return <>{location.pathname}{location.search}; } function open(entry = '/analytics?months=6') { return render(); } beforeEach(() => { localStorage.clear(); mockedApi.get.mockReset(); mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: response(url) }) as any); }); test('[PRES-S01 S02] all existing reports have exact values independent of hidden Dashboard flags', async () => { const key = `dashboardPrefs:${getUserKeyFromToken()}`; localStorage.setItem(key, JSON.stringify({ cards: false, activity: false, funnel: false, companies: false, skills: false })); const stored = localStorage.getItem(key); open(); const activity = await screen.findByRole('region', { name: 'Application activity' }); expect(within(activity).getByText(/1 \/ 3 recorded responses/)).toBeVisible(); expect(within(activity).getByRole('row', { name: 'August 2026 2 1' })).toBeVisible(); for (const name of ['Current pipeline', 'Median current-stage age', 'Response sources', 'Salary insights', 'Top companies by activity', 'Job tags']) expect(screen.getByRole('region', { name })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Faglig vurdering' })).toBeVisible(); expect(screen.getByText('Tag 9')).toBeVisible(); expect(screen.getByRole('row', { name: 'September 2026 1' })).toBeVisible(); expect(screen.queryByRole('button', { name: 'Customize dashboard' })).not.toBeInTheDocument(); expect(mockedApi.get.mock.calls.some(([url]) => String(url).includes('reminders'))).toBe(false); expect(localStorage.getItem(key)).toBe(stored); }); test('[PRES-S01 E07] period is direct-loadable and browser Back restores the period without refetching snapshot', async () => { open('/analytics?months=6&origin=retained'); await screen.findByRole('region', { name: 'Application activity' }); expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/analytics', { params: { months: 6 } }); mockedApi.get.mockClear(); fireEvent.click(screen.getByRole('button', { name: '24 mo' })); await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/tag-trends', { params: { months: 24, limit: 5 } })); expect(screen.getByLabelText('Destination')).toHaveTextContent('/analytics?months=24&origin=retained'); expect(mockedApi.get).not.toHaveBeenCalledWith('/jobapplications/stats'); fireEvent.click(screen.getByRole('button', { name: 'Back' })); await waitFor(() => expect(screen.getByRole('button', { name: '6 mo' })).toHaveAttribute('aria-pressed', 'true')); }); test('[PRES-S01] failed snapshot remains an error while trends and retry stay available', async () => { let fail = true; mockedApi.get.mockImplementation((url: string) => url.endsWith('/stats') && fail ? Promise.reject(new Error('offline')) : Promise.resolve({ data: response(url) }) as any); open(); await screen.findByText('Unable to load current reports'); expect(screen.getByText('Current job reports are unavailable right now. Application activity and tag trends load separately.')).toBeVisible(); expect(screen.queryByText('The dashboard summary is unavailable right now.')).not.toBeInTheDocument(); expect(await screen.findByRole('region', { name: 'Application activity' })).toBeVisible(); expect(screen.queryByText('No tracked jobs to report yet.')).not.toBeInTheDocument(); fail = false; fireEvent.click(screen.getByRole('button', { name: 'Retry' })); expect(await screen.findByRole('link', { name: 'Faglig vurdering' })).toBeVisible(); }); test('[PRES-S01] empty cohorts do not invent a denominator, and unsupported URL periods default safely', async () => { mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url.endsWith('/stats') ? { total: 0, active: 0, byStatus: {} } : url.endsWith('/analytics-overview') ? { funnel: [], responseRateBySource: [], topCompanies: [] } : url.endsWith('/analytics') ? [{ month: '2026-09', applied: 0, responses: 0 }] : url.endsWith('/tag-trends') ? { months: [], series: [] } : [] }) as any); open('/analytics?months=999'); await screen.findByText('No tracked jobs to report yet.'); expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/analytics', { params: { months: 12 } }); expect(screen.queryByText(/0%/)).not.toBeInTheDocument(); expect(screen.getByText('No applications or dated responses in this period.')).toBeVisible(); }); test('[PRES-J05 J09] drill-down retains supported raw status and company identities', async () => { open(); fireEvent.click(await screen.findByRole('link', { name: 'Faglig vurdering' })); expect(screen.getByLabelText('Destination')).toHaveTextContent('/jobs?status=Faglig+vurdering'); fireEvent.click(screen.getByRole('link', { name: 'Nordlys æøå' })); expect(screen.getByLabelText('Destination')).toHaveTextContent('/jobs?companyId=7'); });