fix(detail): render both phases for all-through schools (Batch E)
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 57s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 57s
Batch E of applying the compare-screen learnings to the school detail page — all-through handling (point 11). An all-through school carries both KS2 and KS4 figures in the same yearly rows, but SchoolDetailView flipped it to isSecondary and rendered GCSE-only, hiding the entire primary phase (SATs, phonics, KS2 trend). The Results snapshot already gated its KS2/KS4 blocks purely on data availability, so both already appeared there — but the section title, the trend chart, phonics, the nav label and the history table all still assumed a single phase. - Add an explicit `isAllThrough` flag (+ `showPrimaryContent = isPrimary || isAllThrough`); pure-secondary behaviour is unchanged. - Hero: an "All-through (primary & secondary)" meta chip for all-ages framing. - Results section: title "SATs & GCSE Results", a combined subtitle, and "Primary — KS2 SATs (Year 6)" / "Secondary — GCSEs (Year 11)" sub-headings separating the two blocks. - Results Over Time: render two stacked PerformanceCharts (KS2 SATs + GCSEs) rather than crowding both stages' series — on different scales with different gap stories — onto one axis. Each gets its correct England overlay. - Phonics section + nav item now show for all-through (primary-stage metric). - History table: an all-through column set covering both phases (RWM, Exceeding, Attainment 8, Progress 8, Eng & Maths 4+). - Nav "Results" label for all-through instead of "GCSEs". e2e: a new journey asserts an all-through school (Hessle, 137306) shows both the KS2 and KS4 results and the All-through label, with a data-driven precondition skip if staging data drifts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -148,6 +148,37 @@ test('a report-card school shows its report card, dated to the report-card inspe
|
||||
}
|
||||
});
|
||||
|
||||
test('an all-through school shows BOTH its KS2 SATs and its GCSE results, not just one phase', async ({ page }) => {
|
||||
// All-through schools carry both KS2 and KS4 data in the same yearly rows.
|
||||
// The detail view used to flip them to isSecondary and render GCSE-only,
|
||||
// hiding the primary phase. It now renders both phases and labels the school
|
||||
// "All-through".
|
||||
const AT_URN = 137306; // Hessle High School and Penshurst Primary — all-through
|
||||
const res = await page.request.get(`/api/schools/${AT_URN}`);
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const detail = await res.json();
|
||||
const rows: Array<{ rwm_expected_pct: number | null; attainment_8_score: number | null }> =
|
||||
detail.yearly_data ?? [];
|
||||
const hasKS2 = rows.some((r) => r.rwm_expected_pct != null);
|
||||
const hasKS4 = rows.some((r) => r.attainment_8_score != null);
|
||||
test.skip(
|
||||
(detail.school_info?.phase ?? '').toLowerCase() !== 'all-through' || !hasKS2 || !hasKS4,
|
||||
'precondition: chosen URN must currently be all-through with both KS2 and KS4 results',
|
||||
);
|
||||
|
||||
await page.goto(`/school/${AT_URN}`);
|
||||
await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Labelled as all-through in the hero meta.
|
||||
await expect(page.getByText(/All-through/i).first()).toBeVisible();
|
||||
|
||||
// The combined results section carries both phases.
|
||||
const results = page.locator('#results');
|
||||
await expect(results.getByText(/SATs & GCSE Results/)).toBeVisible();
|
||||
await expect(results.getByText('Reading, Writing & Maths combined')).toBeVisible(); // KS2
|
||||
await expect(results.getByText('Attainment 8').first()).toBeVisible(); // KS4
|
||||
});
|
||||
|
||||
test('school with no performance data still gets a working detail page', async ({ page }) => {
|
||||
// Schools without KS2/KS4 results (special post-16 institutions, sixth-form
|
||||
// centres, PRUs) used to 500 in the API — NaN GIAS fields broke JSON
|
||||
|
||||
@@ -158,10 +158,16 @@ export function SchoolDetailView({
|
||||
|
||||
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
||||
|
||||
// Phase detection
|
||||
// Phase detection. All-through schools cover BOTH key stages, so they are
|
||||
// neither "pure primary" nor "pure secondary": isSecondary stays true (they
|
||||
// have KS4 data) but isAllThrough gates the primary-only content (phonics,
|
||||
// KS2 trend) back on and switches phase-specific copy to an all-ages framing.
|
||||
const phase = schoolInfo.phase ?? '';
|
||||
const isSecondary = phase.toLowerCase().includes('secondary') || phase.toLowerCase() === 'all-through';
|
||||
const isAllThrough = phase.toLowerCase() === 'all-through';
|
||||
const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough;
|
||||
const isPrimary = !isSecondary;
|
||||
// Primary-stage content shows for pure-primary AND all-through schools.
|
||||
const showPrimaryContent = isPrimary || isAllThrough;
|
||||
|
||||
// National averages (fetched dynamically so they stay current)
|
||||
const [nationalAvg, setNationalAvg] = useState<NationalAverages | null>(null);
|
||||
@@ -233,11 +239,11 @@ export function SchoolDetailView({
|
||||
// after the recognised Ofsted badge; low-demand context sections stay last.
|
||||
const navItems: { id: string; label: string }[] = [];
|
||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
||||
if (hasAnyResults) navItems.push({ id: 'results', label: isSecondary ? 'GCSEs' : 'SATs' });
|
||||
if (hasAnyResults) navItems.push({ id: 'results', label: isAllThrough ? 'Results' : isSecondary ? 'GCSEs' : 'SATs' });
|
||||
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
||||
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
|
||||
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
|
||||
if (hasPhonics && isPrimary) navItems.push({ id: 'phonics', label: 'Phonics' });
|
||||
if (hasPhonics && showPrimaryContent) navItems.push({ id: 'phonics', label: 'Phonics' });
|
||||
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
||||
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
||||
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||
@@ -324,6 +330,9 @@ export function SchoolDetailView({
|
||||
{schoolInfo.school_type && (
|
||||
<span className={styles.metaItem}>{schoolInfo.school_type}</span>
|
||||
)}
|
||||
{isAllThrough && (
|
||||
<span className={styles.metaItem}>All-through (primary & secondary)</span>
|
||||
)}
|
||||
{schoolInfo.gender && schoolInfo.gender !== 'Mixed' && (
|
||||
<span className={styles.metaItem}>{schoolInfo.gender}'s school</span>
|
||||
)}
|
||||
@@ -612,10 +621,12 @@ export function SchoolDetailView({
|
||||
{hasAnyResults && latestResults && (
|
||||
<section id="results" className={styles.card}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
{isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
||||
{isAllThrough ? 'SATs & GCSE Results' : isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
||||
</h2>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
{isSecondary
|
||||
{isAllThrough
|
||||
? 'KS2 SATs (end of Year 6) and GCSE results (Year 11) — this school covers both. England averages shown for comparison.'
|
||||
: isSecondary
|
||||
? 'GCSE results for Year 11 pupils. England averages shown for comparison.'
|
||||
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
|
||||
</p>
|
||||
@@ -623,6 +634,9 @@ export function SchoolDetailView({
|
||||
{/* ── Primary / KS2 content ── */}
|
||||
{hasKS2Results && (
|
||||
<>
|
||||
{isAllThrough && (
|
||||
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs (Year 6)</h3>
|
||||
)}
|
||||
<div className={styles.heroStatGrid}>
|
||||
{latestResults.rwm_expected_pct !== null && (
|
||||
<div className={styles.heroStatCard}>
|
||||
@@ -761,6 +775,9 @@ export function SchoolDetailView({
|
||||
{/* ── Secondary / KS4 content ── */}
|
||||
{hasKS4Results && (
|
||||
<>
|
||||
{isAllThrough && (
|
||||
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs (Year 11)</h3>
|
||||
)}
|
||||
<div className={styles.metricsGrid}>
|
||||
{latestResults.attainment_8_score !== null && (
|
||||
<div className={styles.metricCard}>
|
||||
@@ -1046,6 +1063,41 @@ export function SchoolDetailView({
|
||||
{yearlyData.length > 0 && (
|
||||
<section id="history" className={styles.card}>
|
||||
<h2 className={styles.sectionTitle}>Results Over Time</h2>
|
||||
{isAllThrough ? (
|
||||
// All-through: KS2 and KS4 trends are on different scales and have
|
||||
// different gap stories, so render them as two stacked charts
|
||||
// rather than crowding 8+ series onto one axis.
|
||||
<>
|
||||
{hasKS2Results && (
|
||||
<>
|
||||
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs</h3>
|
||||
<div className={styles.chartContainer}>
|
||||
<PerformanceChart
|
||||
data={yearlyData}
|
||||
schoolName={schoolInfo.school_name}
|
||||
isSecondary={false}
|
||||
nationalRwmAvg={primaryAvg.rwm_expected_pct ?? null}
|
||||
nationalByYear={nationalAvg?.by_year}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasKS4Results && (
|
||||
<>
|
||||
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs</h3>
|
||||
<div className={styles.chartContainer}>
|
||||
<PerformanceChart
|
||||
data={yearlyData}
|
||||
schoolName={schoolInfo.school_name}
|
||||
isSecondary={true}
|
||||
nationalAtt8Avg={secondaryAvg.attainment_8_score ?? null}
|
||||
nationalByYear={nationalAvg?.by_year}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.chartContainer}>
|
||||
<PerformanceChart
|
||||
data={yearlyData}
|
||||
@@ -1056,6 +1108,7 @@ export function SchoolDetailView({
|
||||
nationalByYear={nationalAvg?.by_year}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{yearlyData.length > 1 && (
|
||||
<details className={styles.historyDisclosure}>
|
||||
<summary className={styles.historyToggle}>View raw year-by-year data</summary>
|
||||
@@ -1064,7 +1117,15 @@ export function SchoolDetailView({
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Year</th>
|
||||
{isSecondary ? (
|
||||
{isAllThrough ? (
|
||||
<>
|
||||
<th>RWM (expected %)</th>
|
||||
<th>Exceeding (%)</th>
|
||||
<th>Attainment 8</th>
|
||||
<th>Progress 8</th>
|
||||
<th>English & Maths Grade 4+</th>
|
||||
</>
|
||||
) : isSecondary ? (
|
||||
<>
|
||||
<th>Attainment 8</th>
|
||||
<th>Progress 8</th>
|
||||
@@ -1086,7 +1147,15 @@ export function SchoolDetailView({
|
||||
{yearlyData.map((result) => (
|
||||
<tr key={result.year}>
|
||||
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</td>
|
||||
{isSecondary ? (
|
||||
{isAllThrough ? (
|
||||
<>
|
||||
<td>{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}</td>
|
||||
<td>{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}</td>
|
||||
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
||||
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
||||
<td>{result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}</td>
|
||||
</>
|
||||
) : isSecondary ? (
|
||||
<>
|
||||
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
||||
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
||||
@@ -1111,8 +1180,8 @@ export function SchoolDetailView({
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
{/* Year 1 Phonics — primary only */}
|
||||
{hasPhonics && isPrimary && phonics && (
|
||||
{/* Year 1 Phonics — primary-stage metric (pure primary + all-through) */}
|
||||
{hasPhonics && showPrimaryContent && phonics && (
|
||||
<section id="phonics" className={styles.card}>
|
||||
<h2 className={styles.sectionTitle}>Year 1 Phonics ({formatAcademicYear(phonics.year)})</h2>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
|
||||
Reference in New Issue
Block a user