diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 6ae1d16..9e26557 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -40,6 +40,19 @@ async function twoPrimaryUrns(page: Page): Promise<[string, string]> { return [urns[0], urns[1]]; } +async function twoSecondaryUrns(page: Page): Promise<[string, string]> { + const res = await page.request.get('/api/schools?search=school&per_page=100'); + expect(res.ok()).toBeTruthy(); + const body = await res.json(); + const urns: string[] = (body.schools ?? []) + .filter((s: { phase?: string; attainment_8_score?: number | null }) => + s.phase === 'Secondary' && s.attainment_8_score != null, + ) + .map((s: { urn: number }) => String(s.urn)); + expect(urns.length).toBeGreaterThanOrEqual(2); + return [urns[0], urns[1]]; +} + test('home page loads with hero search', async ({ page }) => { await page.goto('/'); await expect(page.locator('h1').first()).toBeVisible(); @@ -199,6 +212,23 @@ test('comparing two schools shows the parent-first sections side by side', async } }); +test('comparing two secondary schools renders the secondary sections', async ({ page }) => { + const [urn0, urn1] = await twoSecondaryUrns(page); + + await page.goto(`/compare?urns=${urn0},${urn1}`); + await expect(page.locator(`a[href*="${urn0}"]`).first()).toBeVisible({ timeout: 15_000 }); + + // The parent-first sections must render — this page was completely blank + // for all-secondary baskets (expert review must-fix #1). + await expect(page.getByRole('heading', { name: 'At a glance' }).first()).toBeVisible({ + timeout: 15_000, + }); + await expect(page.getByRole('heading', { name: 'Ofsted inspection' }).first()).toBeVisible(); + // A KS4 measure proves the secondary academics variant rendered. + await expect(page.getByText(/Attainment 8/i).first()).toBeVisible(); + await expect(page.getByText(/No primary schools in your comparison/)).toHaveCount(0); +}); + test('compare chart on mobile shows school chips with tap-to-focus', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); diff --git a/nextjs-app/__tests__/components/ComparisonView.phase.test.tsx b/nextjs-app/__tests__/components/ComparisonView.phase.test.tsx new file mode 100644 index 0000000..4fd5716 --- /dev/null +++ b/nextjs-app/__tests__/components/ComparisonView.phase.test.tsx @@ -0,0 +1,76 @@ +/** + * Regression: an all-secondary comparison must render the secondary sections. + * + * The basket hydrates from the URL a beat after mount, so the auto-phase + * effect must re-run once selectedSchools arrives — with deps of only + * [comparisonData] it fired once against an empty basket, bailed, and the + * page stayed on an empty "primary" tab ("No primary schools in your + * comparison") even though all schools were secondary. + */ + +import { render, screen, waitFor } from '@testing-library/react'; + +import { ComparisonView } from '@/components/ComparisonView'; +import { ComparisonProvider } from '@/context/ComparisonProvider'; +import type { ComparisonData, School } from '@/lib/types'; + +const fetchComparison = jest.fn(); +jest.mock('@/lib/api', () => ({ + fetchComparison: (...args: unknown[]) => fetchComparison(...args), +})); +jest.mock('@/lib/analytics', () => ({ track: jest.fn() })); + +function secondarySchool(urn: number, name: string): School { + return { + urn, + school_name: name, + local_authority: 'Testshire', + school_type: 'Academy converter', + attainment_8_score: 55, + phase: 'Secondary', + } as School; +} + +function data(urn: number, name: string): ComparisonData { + return { + school_info: secondarySchool(urn, name), + yearly_data: [{ year: 202425, attainment_8_score: 55 }] as ComparisonData['yearly_data'], + ofsted: null, + census: null, + admissions: null, + admissions_history: [], + deprivation: null, + }; +} + +const INITIAL_DATA = { + '300': data(300, 'Gamma High'), + '400': data(400, 'Delta Academy'), +}; + +test('an all-secondary comparison renders the sections, not an empty primary tab', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'At a glance' })).toBeInTheDocument(); + }); + expect(screen.getAllByText('Gamma High').length).toBeGreaterThan(0); + expect(screen.queryByText(/No primary schools in your comparison/)).toBeNull(); + expect(fetchComparison).not.toHaveBeenCalled(); +}); diff --git a/nextjs-app/components/ComparisonView.tsx b/nextjs-app/components/ComparisonView.tsx index 600794e..fd42e1e 100644 --- a/nextjs-app/components/ComparisonView.tsx +++ b/nextjs-app/components/ComparisonView.tsx @@ -173,7 +173,11 @@ export function ComparisonView({ if (!metricFitsPhase) { setSelectedMetric(newPhase === 'secondary' ? 'attainment_8_score' : 'rwm_expected_pct'); } - }, [comparisonData]); // eslint-disable-line react-hooks/exhaustive-deps + // selectedSchools is a dep because the basket hydrates after mount: the + // first run sees an empty basket and bails, so it must re-fire when the + // schools arrive. primarySchools/secondarySchools/metrics/selectedMetric + // are intentionally omitted (derived or would cause loops). + }, [comparisonData, selectedSchools]); // eslint-disable-line react-hooks/exhaustive-deps const handlePhaseChange = (phase: 'primary' | 'secondary') => { phaseLockedByUser.current = true;