Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2208ad93c1 | ||
|
|
24f3cb4c65 | ||
|
|
3816b92d06 | ||
|
|
51ce2d6373 | ||
|
|
5ddc7314fd | ||
|
|
8ad2070e76 | ||
|
|
3aad5101a8 | ||
|
|
c47fe38971 | ||
|
|
8f211577c8 | ||
|
|
9abd020967 |
No files matched your search
@@ -1252,7 +1252,11 @@ test('a published cut-off distance is shown with the year it belongs to', async
|
|||||||
await expect(label).toContainText(`September ${found!.distance.year}`);
|
await expect(label).toContainText(`September ${found!.distance.year}`);
|
||||||
|
|
||||||
// And the figure itself, in the unit councils publish in.
|
// And the figure itself, in the unit councils publish in.
|
||||||
await expect(page.getByText(/\d+(\.\d+)? (miles|m)\b/).first()).toBeVisible();
|
// No trailing \b: the tile's support figure is an adjacent text node, so
|
||||||
|
// the element reads "0.88 miles 1.4 km" and a word boundary after "miles"
|
||||||
|
// is not guaranteed. The leading shape is what matters — a decimal figure
|
||||||
|
// in miles, never a metric one.
|
||||||
|
await expect(page.getByText(/\d+\.\d+ miles/).first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a cut-off distance is never shown without saying it is not a catchment', async ({ page }) => {
|
test('a cut-off distance is never shown without saying it is not a catchment', async ({ page }) => {
|
||||||
@@ -1340,7 +1344,27 @@ test('a school page shows no year-by-year cut-off record', async ({ page }) => {
|
|||||||
// history back without the API.
|
// history back without the API.
|
||||||
await expect(page.getByText(/Last distance offered, by year/)).toHaveCount(0);
|
await expect(page.getByText(/Last distance offered, by year/)).toHaveCount(0);
|
||||||
await expect(page.getByText(/too few to read as a trend/)).toHaveCount(0);
|
await expect(page.getByText(/too few to read as a trend/)).toHaveCount(0);
|
||||||
await expect(page.getByRole('button', { name: 'Distance' })).toHaveCount(0);
|
|
||||||
|
/*
|
||||||
|
* No "Distance" tab in the admissions segmented control.
|
||||||
|
*
|
||||||
|
* Scoped to the control, and exact, because getByRole matches accessible
|
||||||
|
* names by case-insensitive SUBSTRING: an unscoped { name: 'Distance' }
|
||||||
|
* matched "Show this distance on a map" — the map toggle added by this same
|
||||||
|
* feature — and failed a page that was entirely correct. Naming the group
|
||||||
|
* this assertion is about also means unrelated copy elsewhere on the page
|
||||||
|
* can never break it again.
|
||||||
|
*/
|
||||||
|
const viewToggle = page.getByRole('group', { name: 'Admissions view' });
|
||||||
|
const tabs = await viewToggle.getByRole('button').allTextContents();
|
||||||
|
|
||||||
|
// Read the tabs positively rather than asserting an absence against a
|
||||||
|
// locator that might resolve to nothing: if the group selector ever stops
|
||||||
|
// matching, an absence check passes for the wrong reason, which is how the
|
||||||
|
// bug this test is guarding would slip back in unnoticed.
|
||||||
|
expect(tabs, 'admissions view toggle did not resolve').toContain('This year');
|
||||||
|
expect(tabs.map((s) => s.trim()), `admissions tabs: ${tabs.join(', ')}`)
|
||||||
|
.not.toContain('Distance');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the postcode check answers for the published year, and names it', async ({ page }) => {
|
test('the postcode check answers for the published year, and names it', async ({ page }) => {
|
||||||
@@ -1354,7 +1378,7 @@ test('the postcode check answers for the published year, and names it', async ({
|
|||||||
await expect(section).toBeVisible({ timeout: 15_000 });
|
await expect(section).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
await page.getByLabel('Your postcode').fill('SW1A 1AA');
|
await page.getByLabel('Your postcode').fill('SW1A 1AA');
|
||||||
await page.getByRole('button', { name: 'Check' }).click();
|
await page.getByRole('button', { name: 'Check', exact: true }).click();
|
||||||
|
|
||||||
const result = page.getByRole('status');
|
const result = page.getByRole('status');
|
||||||
const error = page.getByRole('alert');
|
const error = page.getByRole('alert');
|
||||||
@@ -1521,6 +1545,32 @@ test('searching a Welsh authority by name returns no schools', async ({ page })
|
|||||||
expect(body.total ?? (body.schools ?? []).length).toBe(0);
|
expect(body.total ?? (body.schools ?? []).length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a Welsh school GIAS does not type as Welsh is gone too', async ({ page }) => {
|
||||||
|
// Beechwood College, Sully, CF64 5SE. GIAS types it "Special post 16
|
||||||
|
// institution" (32), not "Welsh establishment" (30), so the establishment
|
||||||
|
// type filter alone left it behind — it was the last Welsh school standing.
|
||||||
|
const res = await page.request.get('/api/schools?local_authority=Vale%20of%20Glamorgan&page_size=1');
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.total ?? (body.schools ?? []).length).toBe(0);
|
||||||
|
|
||||||
|
const page404 = await page.goto('/school/142458-beechwood-college');
|
||||||
|
expect(page404?.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('English schools with Welsh postcodes are kept', async ({ page }) => {
|
||||||
|
// Royal Mail postcode areas straddle the border: these Gloucestershire
|
||||||
|
// schools carry NP16/NP25 postcodes but are English schools with English
|
||||||
|
// data. A postcode-based filter would have wrongly deleted them, so this
|
||||||
|
// guards the fix from being "simplified" into one.
|
||||||
|
for (const urn of [115639, 115655, 137382]) {
|
||||||
|
const res = await page.request.get(`/api/schools/${urn}`);
|
||||||
|
expect(res.ok(), `URN ${urn} should still resolve`).toBeTruthy();
|
||||||
|
const { school_info } = await res.json();
|
||||||
|
expect(school_info.local_authority).toBe('Gloucestershire');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('a Welsh school URL 404s while an English one still resolves', async ({ page }) => {
|
test('a Welsh school URL 404s while an English one still resolves', async ({ page }) => {
|
||||||
// Paired on purpose: the Welsh assertion alone would also pass if the whole
|
// Paired on purpose: the Welsh assertion alone would also pass if the whole
|
||||||
// site were down, which is the failure this test most needs to distinguish.
|
// site were down, which is the failure this test most needs to distinguish.
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ describe('primary detail page', () => {
|
|||||||
expect(screen.getByText(/Last distance offered/)).toHaveTextContent('September 2024');
|
expect(screen.getByText(/Last distance offered/)).toHaveTextContent('September 2024');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the figure and its metric support readable as two numbers', () => {
|
||||||
|
// They are flex children with a CSS gap and nothing between them in the
|
||||||
|
// text layer, which read as "0.48 miles770 m" to a screen reader and to any
|
||||||
|
// text matcher. Cheap to lose again, so pinned.
|
||||||
|
renderSchoolDetail({ ...primaryFixture, admissionDistance: cutoff({ distance_m: 772.49 }) });
|
||||||
|
|
||||||
|
const tile = document.querySelector('[class*="admissionsTileDistance"]')!;
|
||||||
|
expect(tile.textContent).toMatch(/0\.48 miles\s+770 m/);
|
||||||
|
expect(tile.textContent).not.toMatch(/miles\d/);
|
||||||
|
});
|
||||||
|
|
||||||
it('never shows the figure without saying it is not a catchment', () => {
|
it('never shows the figure without saying it is not a catchment', () => {
|
||||||
renderSchoolDetail({ ...primaryFixture, admissionDistance: cutoff() });
|
renderSchoolDetail({ ...primaryFixture, admissionDistance: cutoff() });
|
||||||
|
|
||||||
|
|||||||
@@ -70,10 +70,16 @@ export function AdmissionsSection({
|
|||||||
{/* Spans both columns rather than taking a half-width cell. This is the
|
{/* Spans both columns rather than taking a half-width cell. This is the
|
||||||
figure parents come to the page for, and at tile width the two-line
|
figure parents come to the page for, and at tile width the two-line
|
||||||
"0.31 miles / September 2025" pairing wraps badly. */}
|
"0.31 miles / September 2025" pairing wraps badly. */}
|
||||||
|
{/* The {' '} between the figure and its metric support is not decoration.
|
||||||
|
The two are flex children, so the gap is drawn by CSS and the text layer
|
||||||
|
had nothing between them: textContent read "0.88 miles1.4 km", which is
|
||||||
|
what a screen reader announces and what any text matcher sees. Whitespace
|
||||||
|
text nodes are not rendered as flex items, so this changes the reading
|
||||||
|
without changing the layout. */}
|
||||||
const distanceTile = cutoff && (
|
const distanceTile = cutoff && (
|
||||||
<div className={`${styles.admissionsTile} ${styles.admissionsTileDistance}`}>
|
<div className={`${styles.admissionsTile} ${styles.admissionsTileDistance}`}>
|
||||||
<dd className={styles.admissionsTileNum}>
|
<dd className={styles.admissionsTileNum}>
|
||||||
{cutoff.primary}
|
{cutoff.primary}{' '}
|
||||||
<span className={styles.admissionsTileSub}>{cutoff.secondary}</span>
|
<span className={styles.admissionsTileSub}>{cutoff.secondary}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt className={styles.admissionsTileLabel}>
|
<dt className={styles.admissionsTileLabel}>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function SecondaryAdmissionsSection({
|
|||||||
Last distance offered · {cutoff.entryYear}
|
Last distance offered · {cutoff.entryYear}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>
|
<div className={styles.metricValue}>
|
||||||
{cutoff.primary}
|
{cutoff.primary}{' '}
|
||||||
<span className={styles.metricValueSub}>{cutoff.secondary}</span>
|
<span className={styles.metricValueSub}>{cutoff.secondary}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,19 @@ clean-targets: ["target", "dbt_packages"]
|
|||||||
vars:
|
vars:
|
||||||
non_england_school_type_codes: [25, 26, 30, 37]
|
non_england_school_type_codes: [25, 26, 30, 37]
|
||||||
|
|
||||||
|
# Type alone is not enough. Beechwood College (URN 142458, Sully, CF64 5SE)
|
||||||
|
# is a Welsh school that GIAS types as a Special post 16 institution (32)
|
||||||
|
# rather than a Welsh establishment (30), so the type filter let it through.
|
||||||
|
# LA code is the reliable discriminator: GIAS gives the 22 Welsh unitary
|
||||||
|
# authorities the contiguous block 660-681, which English authorities never
|
||||||
|
# use (they occupy 201-384 and 800-938).
|
||||||
|
#
|
||||||
|
# Filtering on postcode would be wrong: Redbrook, Tutshill, Wyedean and two
|
||||||
|
# other Gloucestershire schools carry NP16/NP25 postcodes because Royal Mail
|
||||||
|
# areas straddle the border, and they are English schools with English data.
|
||||||
|
welsh_la_code_min: 660
|
||||||
|
welsh_la_code_max: 681
|
||||||
|
|
||||||
models:
|
models:
|
||||||
school_compare:
|
school_compare:
|
||||||
staging:
|
staging:
|
||||||
|
|||||||
@@ -37,3 +37,5 @@ where s.status_code in (1, 3)
|
|||||||
-- coalesce, not a bare NOT IN: a null type code would make the predicate
|
-- coalesce, not a bare NOT IN: a null type code would make the predicate
|
||||||
-- null and drop the row silently. Unknown type is not grounds for exclusion.
|
-- null and drop the row silently. Unknown type is not grounds for exclusion.
|
||||||
and coalesce(s.school_type_code, -1) not in ({{ var('non_england_school_type_codes') | join(', ') }})
|
and coalesce(s.school_type_code, -1) not in ({{ var('non_england_school_type_codes') | join(', ') }})
|
||||||
|
-- Welsh schools GIAS does not type as Welsh establishments (see vars).
|
||||||
|
and coalesce(s.local_authority_code, -1) not between {{ var('welsh_la_code_min') }} and {{ var('welsh_la_code_max') }}
|
||||||
@@ -96,3 +96,5 @@ where s.status_code in (1, 3)
|
|||||||
-- coalesce, not a bare NOT IN: a null type code would make the predicate
|
-- coalesce, not a bare NOT IN: a null type code would make the predicate
|
||||||
-- null and drop the row silently. Unknown type is not grounds for exclusion.
|
-- null and drop the row silently. Unknown type is not grounds for exclusion.
|
||||||
and coalesce(s.school_type_code, -1) not in ({{ var('non_england_school_type_codes') | join(', ') }})
|
and coalesce(s.school_type_code, -1) not in ({{ var('non_england_school_type_codes') | join(', ') }})
|
||||||
|
-- Welsh schools GIAS does not type as Welsh establishments (see vars).
|
||||||
|
and coalesce(s.local_authority_code, -1) not between {{ var('welsh_la_code_min') }} and {{ var('welsh_la_code_max') }}
|
||||||
@@ -26,3 +26,29 @@ select
|
|||||||
from {{ ref('dim_location') }} l
|
from {{ ref('dim_location') }} l
|
||||||
left join {{ ref('dim_school') }} s on l.urn = s.urn
|
left join {{ ref('dim_school') }} s on l.urn = s.urn
|
||||||
where s.urn is null
|
where s.urn is null
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
-- Belt and braces on the Welsh LA code range.
|
||||||
|
--
|
||||||
|
-- The range 660-681 is documented GIAS structure, not something this project
|
||||||
|
-- can verify from its own data — local_authority_code is not exposed anywhere
|
||||||
|
-- the app can assert on. So the range does the filtering and the authority
|
||||||
|
-- NAME does the checking: if 660-681 is ever wrong or incomplete, a Welsh
|
||||||
|
-- authority reappears here and the pipeline fails loudly rather than the
|
||||||
|
-- corpus quietly regrowing a Welsh tail.
|
||||||
|
--
|
||||||
|
-- Beechwood College (142458) is why this exists: a Welsh school GIAS types as
|
||||||
|
-- a Special post 16 institution, which the type filter alone let through.
|
||||||
|
select
|
||||||
|
l.urn,
|
||||||
|
l.local_authority_name as school_name,
|
||||||
|
null::integer as school_type_code
|
||||||
|
from {{ ref('dim_location') }} l
|
||||||
|
where l.local_authority_name in (
|
||||||
|
'Isle of Anglesey', 'Gwynedd', 'Conwy', 'Denbighshire', 'Flintshire',
|
||||||
|
'Wrexham', 'Powys', 'Ceredigion', 'Pembrokeshire', 'Carmarthenshire',
|
||||||
|
'Swansea', 'Neath Port Talbot', 'Bridgend', 'Vale of Glamorgan',
|
||||||
|
'Rhondda Cynon Taf', 'Merthyr Tydfil', 'Caerphilly', 'Blaenau Gwent',
|
||||||
|
'Torfaen', 'Monmouthshire', 'Newport', 'Cardiff'
|
||||||
|
)
|
||||||
Reference in new issue
Block a user