Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
914b429a28 |
No files matched your search
@@ -121,33 +121,6 @@ test('the home page has a real title', async ({ page }) => {
|
|||||||
expect(await page.title()).toMatch(/schoolcompare/i);
|
expect(await page.title()).toMatch(/schoolcompare/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the site states its independence and never claims to be official', async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
// Carried in the footer, so it appears on every page rather than only here.
|
|
||||||
await expect(
|
|
||||||
page.getByText(/an independent site\.\s*not affiliated with the department for education/i)
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Describing the DATA as official is accurate and appears throughout
|
|
||||||
* ("official DfE figures", "the official figure isn't in"). Describing the
|
|
||||||
* SITE as official is not: schoolcompare republishes government data, it is
|
|
||||||
* not a government service. The difference is the grammatical subject, so
|
|
||||||
* this looks for text that opens with "Official" as a standalone label —
|
|
||||||
* the shape a value-prop title or heading takes, and the shape the old
|
|
||||||
* "Official & trusted" took.
|
|
||||||
*/
|
|
||||||
const claims = await page.evaluate(() =>
|
|
||||||
[...document.querySelectorAll('body *')]
|
|
||||||
.flatMap(el => [...el.childNodes]
|
|
||||||
.filter(n => n.nodeType === 3)
|
|
||||||
.map(n => (n.textContent ?? '').trim()))
|
|
||||||
.filter(t => /^Official\b/i.test(t) && !/^Official\s+(DfE|data|figures)/i.test(t))
|
|
||||||
);
|
|
||||||
expect(claims, `text presenting the site itself as official: ${claims.join(' | ')}`).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the landing bands share one vertical rhythm', async ({ page }) => {
|
test('the landing bands share one vertical rhythm', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
const gaps = await page.evaluate(() => {
|
const gaps = await page.evaluate(() => {
|
||||||
@@ -816,21 +789,9 @@ test('the brand asset set is complete and served', async ({ page }) => {
|
|||||||
expect(og.ok()).toBe(true);
|
expect(og.ok()).toBe(true);
|
||||||
expect(og.headers()['content-type']).toContain('image/png');
|
expect(og.headers()['content-type']).toContain('image/png');
|
||||||
|
|
||||||
/*
|
// iOS ignores SVG touch icons, so this must be a real raster.
|
||||||
* iOS ignores SVG touch icons, so this must be a real raster.
|
const apple = await page.request.get('/apple-icon');
|
||||||
*
|
expect(apple.ok()).toBe(true);
|
||||||
* Read the href off the page rather than hardcoding a path. The route moved
|
|
||||||
* when the generated app/apple-icon.tsx became a static app/apple-icon.png:
|
|
||||||
* a generated icon is served at /apple-icon, a static one at /apple-icon.png
|
|
||||||
* with a content hash. This test kept asking for /apple-icon and got a 404 —
|
|
||||||
* reporting a missing icon while the icon was present and correctly linked.
|
|
||||||
*/
|
|
||||||
const appleLink = page.locator('link[rel="apple-touch-icon"]');
|
|
||||||
await expect(appleLink).toHaveCount(1);
|
|
||||||
const appleHref = await appleLink.getAttribute('href');
|
|
||||||
expect(appleHref).toBeTruthy();
|
|
||||||
const apple = await page.request.get(appleHref!);
|
|
||||||
expect(apple.ok(), `apple touch icon at ${appleHref} should be served`).toBe(true);
|
|
||||||
expect(apple.headers()['content-type']).toContain('image/png');
|
expect(apple.headers()['content-type']).toContain('image/png');
|
||||||
|
|
||||||
// Android needs a maskable PNG or the install prompt has no icon.
|
// Android needs a maskable PNG or the install prompt has no icon.
|
||||||
@@ -951,22 +912,8 @@ test('the header carries the schoolcompare lockup', async ({ page }) => {
|
|||||||
// hashed at build time and change on any unrelated edit.
|
// hashed at build time and change on any unrelated edit.
|
||||||
await expect(home).toHaveText(/^\s*schoolcompare\s*$/);
|
await expect(home).toHaveText(/^\s*schoolcompare\s*$/);
|
||||||
|
|
||||||
/*
|
const mark = home.locator('svg').first();
|
||||||
* The mark is raster artwork, not an inline SVG — it has been since the
|
|
||||||
* supplied logo replaced the reconstruction, and this locator went on
|
|
||||||
* looking for an <svg> that no longer exists.
|
|
||||||
*
|
|
||||||
* Assert on naturalWidth rather than visibility: a <picture> whose sources
|
|
||||||
* all 404 still lays out and still passes toBeVisible(), so the weaker
|
|
||||||
* assertion would go green on a broken lockup.
|
|
||||||
*/
|
|
||||||
const mark = home.locator('img').first();
|
|
||||||
await expect(mark).toBeVisible();
|
await expect(mark).toBeVisible();
|
||||||
const markState = await mark.evaluate((el: HTMLImageElement) => ({
|
|
||||||
loaded: el.complete && el.naturalWidth > 0,
|
|
||||||
src: el.currentSrc,
|
|
||||||
}));
|
|
||||||
expect(markState.loaded, `header mark failed to load: ${markState.src}`).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('no visible text falls back to the browser default black', async ({ page }) => {
|
test('no visible text falls back to the browser default black', async ({ page }) => {
|
||||||
|
|||||||
@@ -460,30 +460,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Narrow phones: the button drops below the input instead of sharing the row.
|
|
||||||
*
|
|
||||||
* "Search schools" is a fixed 134px with `white-space: nowrap`, so on a 390px
|
|
||||||
* screen it took 48% of the row and left the input 102px of text space for a
|
|
||||||
* 194px placeholder — the field showed "School name or" and stopped. At 320px
|
|
||||||
* the input was down to 32px, which is too narrow to read what you are typing.
|
|
||||||
*
|
|
||||||
* Wrapping rather than restructuring: the pill stays one element, so it keeps
|
|
||||||
* its border, shadow and :focus-within ring, and the button gets a full-width
|
|
||||||
* tap target on the way. Giving the button a 100% flex-basis is what forces
|
|
||||||
* the wrap; the input keeps `flex: 1` and so takes the whole first row.
|
|
||||||
*/
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.heroMode .omniBoxContainer {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.heroMode .searchButton {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
@media (max-width: 420px) {
|
||||||
/* Below this the pin costs more room than it earns. */
|
/* Below this the pin costs more room than it earns. */
|
||||||
.heroMode .omniIcon {
|
.heroMode .omniIcon {
|
||||||
|
|||||||
@@ -92,22 +92,6 @@
|
|||||||
max-width: 34ch;
|
max-width: 34ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Quieter than .description, but by size only — not by contrast.
|
|
||||||
*
|
|
||||||
* --on-sunken-faint measured 4.71:1 here, which clears AA by 0.21. That is a
|
|
||||||
* fine margin for decorative text and the wrong one for a disclaimer, whose
|
|
||||||
* whole job is to be legible to someone checking whether this is a government
|
|
||||||
* site. --on-sunken-muted is 5.66:1 on the same band.
|
|
||||||
*/
|
|
||||||
.independence {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--step--2);
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--on-sunken-muted);
|
|
||||||
max-width: 38ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
|
|||||||
@@ -35,14 +35,6 @@ export function Footer() {
|
|||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
Compare primary and secondary schools across England.
|
Compare primary and secondary schools across England.
|
||||||
</p>
|
</p>
|
||||||
{/*
|
|
||||||
Says once, plainly, what the landing page's value props only
|
|
||||||
imply: we publish official data, we are not an official body.
|
|
||||||
Cheap to state and expensive to be wrong about.
|
|
||||||
*/}
|
|
||||||
<p className={styles.independence}>
|
|
||||||
An independent site. Not affiliated with the Department for Education or Ofsted.
|
|
||||||
</p>
|
|
||||||
<a
|
<a
|
||||||
href="mailto:contact@schoolcompare.co.uk"
|
href="mailto:contact@schoolcompare.co.uk"
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
|
|||||||
@@ -161,22 +161,10 @@
|
|||||||
* page and out-shouts the H1 and the search box. Dimming it in CSS is the
|
* page and out-shouts the H1 and the search box. Dimming it in CSS is the
|
||||||
* whole treatment, and the scrim then fades it into the dark panel instead of
|
* whole treatment, and the scrim then fades it into the dark panel instead of
|
||||||
* into cream.
|
* into cream.
|
||||||
*
|
|
||||||
* Raising the brightness raises the background under light dark-theme text, so
|
|
||||||
* this value has a contrast floor, not just a taste range. Measured off
|
|
||||||
* rendered pixels, sampling background up to 120px past each line:
|
|
||||||
*
|
|
||||||
* brightness title body
|
|
||||||
* 0.52 11.01:1 6.44:1
|
|
||||||
* 0.75 9.48:1 5.21:1 ← current
|
|
||||||
*
|
|
||||||
* Body text is the binding one. Going much above 0.75 walks it toward the
|
|
||||||
* 4.5:1 floor, and at that point the scrim needs to carry further right rather
|
|
||||||
* than the artwork being dimmed less.
|
|
||||||
*/
|
*/
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.heroArt img {
|
.heroArt img {
|
||||||
filter: brightness(0.75) saturate(0.72) contrast(1.02);
|
filter: brightness(0.52) saturate(0.72) contrast(1.02);
|
||||||
}
|
}
|
||||||
.heroPanel::before {
|
.heroPanel::before {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
@@ -189,43 +177,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* The one line in the first person, so it is set apart from the marketing copy
|
|
||||||
* around it without shouting: display face, a step down in size, and a short
|
|
||||||
* brand rule instead of a bullet or an emoji.
|
|
||||||
*
|
|
||||||
* No italic — Manrope ships no italic in the loaded weights, so font-style
|
|
||||||
* would be synthesised into a slant. Same reason .heroEmph sets font-style
|
|
||||||
* back to normal.
|
|
||||||
*/
|
|
||||||
.heroByline {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
margin: 1.25rem 0 0;
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--step--1);
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heroByline::before {
|
|
||||||
content: '';
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 1px;
|
|
||||||
background: var(--brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.heroByline {
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: var(--step--2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.heroEyebrow {
|
.heroEyebrow {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -302,30 +253,12 @@
|
|||||||
* the same decision.
|
* the same decision.
|
||||||
*/
|
*/
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
/*
|
|
||||||
* Artwork above the copy, not below it.
|
|
||||||
*
|
|
||||||
* The DOM keeps .heroContent first so the desktop overlay does not depend on
|
|
||||||
* source order, which left the band stranded under the search on phones —
|
|
||||||
* reading as a strip stuck on the end rather than as a hero image. `order`
|
|
||||||
* moves it visually only; it is decorative and aria-hidden, so there is no
|
|
||||||
* reading order to disturb.
|
|
||||||
*
|
|
||||||
* It costs about 136px above the search. Measured on a 667px viewport (the
|
|
||||||
* shortest phone still in use) the search still lands near 400px, well
|
|
||||||
* inside the fold.
|
|
||||||
*/
|
|
||||||
.heroPanel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.heroContent {
|
.heroContent {
|
||||||
padding: 1.75rem 1.5rem 1.75rem;
|
padding: 2rem 1.5rem 1.75rem;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
.heroArt {
|
.heroArt {
|
||||||
position: static;
|
position: static;
|
||||||
order: -1;
|
|
||||||
height: 11rem;
|
height: 11rem;
|
||||||
}
|
}
|
||||||
/* The band crop centres the schoolhouse at about 65% across, so squeezing
|
/* The band crop centres the schoolhouse at about 65% across, so squeezing
|
||||||
|
|||||||
@@ -157,20 +157,7 @@ const VALUE_PROPS: ValueProp[] = [
|
|||||||
{
|
{
|
||||||
icon: <ShieldCheckIcon />,
|
icon: <ShieldCheckIcon />,
|
||||||
tintClass: styles.propIconTrust,
|
tintClass: styles.propIconTrust,
|
||||||
/*
|
title: 'Official & trusted',
|
||||||
* "Built on official data", never "Official".
|
|
||||||
*
|
|
||||||
* The previous title was "Official & trusted", whose grammatical subject
|
|
||||||
* is this site — it reads as a claim that schoolcompare is itself an
|
|
||||||
* official service. It is not: it is an independent site that republishes
|
|
||||||
* official figures. The distinction is the difference between describing
|
|
||||||
* the data and describing ourselves, and only the first is true.
|
|
||||||
*
|
|
||||||
* Everywhere else the word appears ("official DfE figures", "the official
|
|
||||||
* figure isn't in") it already qualifies the data, which is correct and
|
|
||||||
* should stay.
|
|
||||||
*/
|
|
||||||
title: 'Built on official data',
|
|
||||||
body: 'Every figure comes from DfE performance tables and Ofsted.',
|
body: 'Every figure comes from DfE performance tables and Ofsted.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -464,19 +451,6 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
|||||||
geoError={geoError}
|
geoError={geoError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/*
|
|
||||||
Sits under the search rather than in the footer on purpose: this
|
|
||||||
is the one line on the page written in the first person, and it
|
|
||||||
earns its place at the moment someone is deciding whether to
|
|
||||||
trust a site full of government statistics. In the footer it
|
|
||||||
would be true and unread.
|
|
||||||
|
|
||||||
It is deliberately the only such claim — the page's other trust
|
|
||||||
signals are all about the data's provenance, which is checkable.
|
|
||||||
This one is about who made it, which is not, so it is stated
|
|
||||||
once, plainly, and never repeated.
|
|
||||||
*/}
|
|
||||||
<p className={styles.heroByline}>Built for parents, by a parent.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
|
|||||||
Reference in new issue
Block a user