80 lines
6.5 KiB
TypeScript
80 lines
6.5 KiB
TypeScript
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<typeof api>;
|
|
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 <><output aria-label="Destination">{location.pathname}{location.search}</output><button onClick={() => navigate(-1)}>Back</button></>; }
|
|
function open(entry = '/analytics?months=6') { return render(<I18nProvider><MemoryRouter initialEntries={[entry]}><History /><AnalyticsPage /></MemoryRouter></I18nProvider>); }
|
|
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');
|
|
});
|