test(e2e): cover the similar-schools section, compare hand-off and mobile widths
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 37s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m15s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 3m1s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 37s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m15s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 3m1s
Two things here cannot be covered anywhere else. jsdom has no layout, so scrollWidth and clientWidth are both 0 and the arrows' disabled state can only be measured by a real engine. And the scroll position surviving a selection is DOM state rather than React state, so only a real browser can prove the row does not jump back when the footer re-renders. MOBILE.md asks for a Playwright width check and records that it was not written because Playwright was not in the project. It is — this suite — so the check exists now, scoped to the page this feature touches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e0d8bcf87
commit
bd2a6c385b
2 files changed
+114
-1
No files matched your search
@@ -73,7 +73,12 @@ There is no SWR dependency. Leaflet maps are loaded through dynamic wrappers;
|
|||||||
Chart.js renders performance and comparison charts.
|
Chart.js renders performance and comparison charts.
|
||||||
|
|
||||||
`components/school/` contains detail sections, with section decisions and data
|
`components/school/` contains detail sections, with section decisions and data
|
||||||
preparation in `lib/schoolSections.ts`. `lib/types.ts` contains manually maintained
|
preparation in `lib/schoolSections.ts`. The similar-schools section is selected
|
||||||
|
in `backend/similar_schools.py` — hard filters that never relax (phase,
|
||||||
|
provision, selectivity, gender) and soft preferences that do (religious
|
||||||
|
character, then gender exactness) — and served on `/api/schools/{urn}`. Its
|
||||||
|
rules are presentation logic, deliberately kept out of `marts.*` so they can be
|
||||||
|
tuned by deploy rather than by pipeline run. `lib/types.ts` contains manually maintained
|
||||||
API types. `payload-types.ts` and the Payload import map are generated artifacts.
|
API types. `payload-types.ts` and the Payload import map are generated artifacts.
|
||||||
|
|
||||||
## Publication and caching today
|
## Publication and caching today
|
||||||
|
|||||||
@@ -2734,3 +2734,111 @@ test('the content sitemap lists the about page and is advertised in robots', asy
|
|||||||
expect(body).toContain('/sitemap.xml');
|
expect(body).toContain('/sitemap.xml');
|
||||||
expect(body).toContain('/content-sitemap.xml');
|
expect(body).toContain('/content-sitemap.xml');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar schools nearby.
|
||||||
|
*
|
||||||
|
* The section is absent by design where fewer than two schools qualify, and the
|
||||||
|
* arrows are absent where three cards fit, so this asserts each part of the
|
||||||
|
* contract only where it applies.
|
||||||
|
*
|
||||||
|
* Two things here cannot be tested anywhere else: the arrows' disabled state,
|
||||||
|
* which jsdom cannot measure because it has no layout, and the scroll position
|
||||||
|
* surviving a selection, which is DOM state rather than React state.
|
||||||
|
*/
|
||||||
|
test('similar schools link on to other schools and into compare', async ({ page }) => {
|
||||||
|
await searchByName(page, 'Primary');
|
||||||
|
await schoolLinks(page).first().click();
|
||||||
|
await page.waitForURL(/\/school\//);
|
||||||
|
|
||||||
|
const section = page.locator('#similar');
|
||||||
|
if ((await section.count()) === 0) {
|
||||||
|
test.skip(true, 'No qualifying similar schools for this school');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every card is a real link to another school page — including the ones
|
||||||
|
// behind the arrows, which is the whole reason this is a scroller and not a
|
||||||
|
// paginated widget.
|
||||||
|
const links = section.locator('a[href^="/school/"]');
|
||||||
|
const linkCount = await links.count();
|
||||||
|
expect(linkCount).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(linkCount).toBeLessThanOrEqual(6);
|
||||||
|
expect(await links.first().getAttribute('href')).toMatch(/^\/school\/\d{6}-/);
|
||||||
|
|
||||||
|
await expect(section.getByText(/miles away/).first()).toBeVisible();
|
||||||
|
|
||||||
|
const scroller = section.locator('ul').first();
|
||||||
|
|
||||||
|
// The carousel, where this school had more than three matches.
|
||||||
|
const forward = section.getByRole('button', { name: 'More schools' });
|
||||||
|
if (await forward.count()) {
|
||||||
|
const back = section.getByRole('button', { name: 'Previous schools' });
|
||||||
|
await expect(back).toBeDisabled();
|
||||||
|
|
||||||
|
await forward.click();
|
||||||
|
await expect
|
||||||
|
.poll(() => scroller.evaluate((node: HTMLElement) => node.scrollLeft))
|
||||||
|
.toBeGreaterThan(8);
|
||||||
|
await expect(back).toBeEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The compare hand-off, and the row must not jump back to the start when the
|
||||||
|
// footer re-renders underneath it.
|
||||||
|
const offsetBefore = await scroller.evaluate((node: HTMLElement) => node.scrollLeft);
|
||||||
|
await section.getByRole('button', { name: /Add to compare/ }).first().click();
|
||||||
|
await expect(
|
||||||
|
section.getByRole('button', { name: /Added to compare/ }).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
expect(await scroller.evaluate((node: HTMLElement) => node.scrollLeft)).toBe(offsetBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The section at MOBILE.md's three reference widths.
|
||||||
|
*
|
||||||
|
* MOBILE.md asks for exactly this check and records that it was not written
|
||||||
|
* because "Playwright isn't currently in the project dependency set". That is
|
||||||
|
* no longer true — this suite is Playwright — so the check exists now, scoped
|
||||||
|
* to the page this feature touches.
|
||||||
|
*/
|
||||||
|
for (const width of [360, 390, 430]) {
|
||||||
|
test(`similar schools survives a ${width}px viewport`, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width, height: 800 });
|
||||||
|
await searchByName(page, 'Primary');
|
||||||
|
await schoolLinks(page).first().click();
|
||||||
|
await page.waitForURL(/\/school\//);
|
||||||
|
|
||||||
|
const section = page.locator('#similar');
|
||||||
|
if ((await section.count()) === 0) {
|
||||||
|
test.skip(true, 'No qualifying similar schools for this school');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Nothing bleeds past the right edge.
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth),
|
||||||
|
).toBe(0);
|
||||||
|
|
||||||
|
// 2. No arrows on touch widths — swiping does the job, and they would take
|
||||||
|
// 96px from a 328px card.
|
||||||
|
await expect(section.getByRole('button', { name: 'More schools' })).toHaveCount(0);
|
||||||
|
|
||||||
|
// 3. Every tap target in the section clears 44px. A card title's own box is
|
||||||
|
// shorter, but its hit area is the whole card via ::after.
|
||||||
|
const failing = await section.evaluate((root: HTMLElement) =>
|
||||||
|
Array.from(root.querySelectorAll('a, button'))
|
||||||
|
.filter((el) => (el as HTMLElement).offsetParent)
|
||||||
|
.map((el) => {
|
||||||
|
const card = el.closest('li');
|
||||||
|
const box = el.matches('h3 a') && card
|
||||||
|
? card.getBoundingClientRect()
|
||||||
|
: el.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
text: (el as HTMLElement).innerText.trim().slice(0, 24),
|
||||||
|
w: box.width,
|
||||||
|
h: box.height,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((o) => o.w < 44 || o.h < 44),
|
||||||
|
);
|
||||||
|
expect(failing).toEqual([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user