fix(admissions): move the cut-off detail into its own section
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m5s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 13s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m59s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m5s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 13s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m59s
The admissions card measured 1503px on a live school page — half the height of every section put together, and nearly three times the next largest — with its default view rendering as four tiles adrift in about 1080px of blank card. The cause was a layout trick meeting content it was never sized for. The admissions views are stacked in one grid cell so switching them never shifts layout, and the hidden ones keep their box: only visibility is dropped. That works while the views are comparable. The distance view added in #102 carries a chart, a table and a map, came to 1402px against the tile grid's 316px, and pinned every other view to its height — including the one that renders by default, which nobody had clicked. Rather than only unpinning it, the detail moves out. Every other topic on the page is a section with a nav entry, and "how close did we need to live, and would we have got in?" is a topic, not a variant reading of the intake figures. The headline number stays on the Admissions tile where the intake story is; the record behind it now lives in a Distance section directly below. admissions 1503px -> 554px distance new -> 743px (median section on the page is ~528px) Three further changes, each of which also makes the content better rather than only shorter: * The map renders on request. Before a postcode is entered it is a circle drawn round a school, and it costs a Leaflet bundle and 240px to say so; a successful check opens it automatically, which is the point at which it starts answering something. Map height 320px -> 240px. * The chart appears only at the four published years that let the summary state a direction. Below that we already refuse to call the series a trend, and a line through three points asserts one regardless of what the sentence beneath it admits. The table carries those years anyway, with the reasons a line cannot show. * Three caveat paragraphs become one. They said walking-route twice and made the same point about priorities in two voices. It now sits in CutoffDistanceDetail rather than inside the check, so it still renders for a school with coordinates missing, where there is a table but no map and no check. The new e2e guard asserts no section exceeds 2.5x the median section height. Measuring the card's internals cannot catch this: the tile grid is flex: 1, so it absorbs the stretch and every box still looks full. The first version of this test targeted an arbitrary primary, passed against the live bug, and proved nothing; pointed at a school that actually holds cut-off history it fails on staging with "#admissions is 1459px against a 526px median". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
This commit is contained in:
1 parent
8bf6145a73
commit
94151c58ea
14 files changed
+403
-185
No files matched your search
+57
-27
@@ -1324,16 +1324,13 @@ test('a school with several published years gets the Distance view', async ({ pa
|
||||
await page.goto(`/school/${found!.urn}`);
|
||||
await expect(page.locator('#admissions')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const isSecondary = /secondary/i.test(found!.phase ?? '');
|
||||
if (!isSecondary) {
|
||||
// Primary pages carry a segmented control; the detail sits behind it.
|
||||
const distanceTab = page.getByRole('button', { name: 'Distance' });
|
||||
await expect(distanceTab).toBeVisible();
|
||||
await distanceTab.click();
|
||||
await expect(distanceTab).toHaveAttribute('aria-pressed', 'true');
|
||||
}
|
||||
// Its own section on both templates, not a third tab inside Admissions —
|
||||
// stacking a view that tall in the admissions viewport sized the whole card
|
||||
// to it and left the default view mostly blank.
|
||||
await expect(page.locator('#distance')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Distance' })).toHaveCount(0);
|
||||
|
||||
// Every published year must appear as a row, whichever template rendered it.
|
||||
// Every published year must appear as a row.
|
||||
for (const h of found!.history as { year: number }[]) {
|
||||
await expect(page.getByRole('rowheader', { name: String(h.year) })).toBeVisible();
|
||||
}
|
||||
@@ -1345,10 +1342,7 @@ test('the year table never leaves a gap unexplained', async ({ page }) => {
|
||||
test.skip(found === null, 'no school in the sample has 2+ published cut-off years yet');
|
||||
|
||||
await page.goto(`/school/${found!.urn}`);
|
||||
await expect(page.locator('#admissions')).toBeVisible({ timeout: 15_000 });
|
||||
if (!/secondary/i.test(found!.phase ?? '')) {
|
||||
await page.getByRole('button', { name: 'Distance' }).click();
|
||||
}
|
||||
await expect(page.locator('#distance')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const statuses = await page.locator('[class*="cutoffPill"]').allTextContents();
|
||||
expect(statuses.length).toBeGreaterThan(0);
|
||||
@@ -1364,10 +1358,7 @@ test('the postcode check answers with a distance and per-year verdicts', async (
|
||||
test.skip(found === null, 'no school in the sample has 2+ published cut-off years yet');
|
||||
|
||||
await page.goto(`/school/${found!.urn}`);
|
||||
await expect(page.locator('#admissions')).toBeVisible({ timeout: 15_000 });
|
||||
if (!/secondary/i.test(found!.phase ?? '')) {
|
||||
await page.getByRole('button', { name: 'Distance' }).click();
|
||||
}
|
||||
await expect(page.locator('#distance')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const input = page.getByLabel('Your postcode');
|
||||
await expect(input).toBeVisible();
|
||||
@@ -1391,13 +1382,14 @@ test('the postcode check states its limits before it is used', async ({ page })
|
||||
test.skip(found === null, 'no school in the sample has 2+ published cut-off years yet');
|
||||
|
||||
await page.goto(`/school/${found!.urn}`);
|
||||
await expect(page.locator('#admissions')).toBeVisible({ timeout: 15_000 });
|
||||
if (!/secondary/i.test(found!.phase ?? '')) {
|
||||
await page.getByRole('button', { name: 'Distance' }).click();
|
||||
}
|
||||
await expect(page.locator('#distance')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await expect(page.getByText(/An indication only/)).toBeVisible();
|
||||
await expect(page.getByText(/not a catchment boundary/)).toBeVisible();
|
||||
const caveat = page.getByText(/Distance is the last criterion applied/);
|
||||
await expect(caveat).toBeVisible();
|
||||
await expect(caveat).toContainText(/not a catchment boundary/);
|
||||
await expect(caveat).toContainText(/walking route/);
|
||||
// One caveat for the section, not the three paragraphs it replaced.
|
||||
await expect(caveat).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('the distance section never scrolls the page sideways', async ({ page }) => {
|
||||
@@ -1406,10 +1398,7 @@ test('the distance section never scrolls the page sideways', async ({ page }) =>
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(`/school/${found!.urn}`);
|
||||
await expect(page.locator('#admissions')).toBeVisible({ timeout: 15_000 });
|
||||
if (!/secondary/i.test(found!.phase ?? '')) {
|
||||
await page.getByRole('button', { name: 'Distance' }).click();
|
||||
}
|
||||
await expect(page.locator('#distance')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// The year table is deliberately wider than a phone; its own wrapper has to
|
||||
// absorb that, or the whole page slides under the reader's thumb.
|
||||
@@ -1417,3 +1406,44 @@ test('the distance section never scrolls the page sideways', async ({ page }) =>
|
||||
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
expect(overflow, 'page must not scroll horizontally').toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('no single section dominates the height of a school page', async ({ page }) => {
|
||||
/*
|
||||
* The admissions views are stacked in one grid cell so switching them never
|
||||
* shifts layout, which means the card is sized by its TALLEST view while the
|
||||
* hidden ones keep their box. A distance view carrying a chart, a table and a
|
||||
* map was added there and measured 1402px against the tile grid's 316px; the
|
||||
* DEFAULT view rendered as four tiles adrift in ~1080px of blank card, and
|
||||
* Admissions alone came to half the height of every section on the page
|
||||
* (1503px against 526px for the next largest).
|
||||
*
|
||||
* Measuring the card's internals cannot catch it: the tile grid is
|
||||
* `flex: 1`, so it absorbs the stretch and every box still looks full. What
|
||||
* a reader actually sees is one section wildly out of proportion with its
|
||||
* neighbours, so that is what this asserts.
|
||||
*/
|
||||
const found = await schoolWithCutoff(page, 2);
|
||||
test.skip(found === null, 'no school in the sample has 2+ published cut-off years yet');
|
||||
|
||||
await page.goto(`/school/${found!.urn}`);
|
||||
await expect(page.locator('#admissions')).toBeVisible({ timeout: 15_000 });
|
||||
await page.waitForTimeout(1200); // charts settle, and they carry real height
|
||||
|
||||
const sections = await page.evaluate(() =>
|
||||
[...document.querySelectorAll('section[id]')]
|
||||
.map((s) => ({ id: s.id, h: Math.round(s.getBoundingClientRect().height) }))
|
||||
.filter((s) => s.h > 0));
|
||||
|
||||
expect(sections.length).toBeGreaterThanOrEqual(3);
|
||||
const heights = sections.map((s) => s.h).sort((a, b) => a - b);
|
||||
const median = heights[Math.floor(heights.length / 2)];
|
||||
const worst = sections.reduce((a, b) => (a.h > b.h ? a : b));
|
||||
|
||||
// Generous: a rich section may fairly run to twice a plain one. Nearly three
|
||||
// times over is the shape of a layout fault, not of denser content.
|
||||
expect(
|
||||
worst.h / median,
|
||||
`#${worst.id} is ${worst.h}px against a ${median}px median: `
|
||||
+ sections.map((s) => `${s.id}=${s.h}`).join(', '),
|
||||
).toBeLessThan(2.5);
|
||||
});
|
||||
Reference in new issue
Block a user