test(e2e): cover the detail page server/client boundary
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m7s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 14s
PR Checks / Build Frontend (no push) (pull_request) Successful in 53s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m14s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 3m23s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m7s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 14s
PR Checks / Build Frontend (no push) (pull_request) Successful in 53s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m14s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 3m23s
Two journeys for what the split actually changed: the admissions toggle (the one client island inside an otherwise server-rendered section) and the sticky nav resolving to server-rendered sections. The nav is client-rendered from a server-computed list while the sections render on the server, so a mismatch between the two halves would only show up in a real browser. Existing journeys already cover all-through and special-school pages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -556,3 +556,73 @@ test('compare metric-help popover stays within the mobile viewport', async ({ pa
|
||||
.evaluate((el) => el.scrollWidth > el.clientWidth + 1);
|
||||
expect(bodyOverflowsX).toBe(false);
|
||||
});
|
||||
|
||||
/**
|
||||
* The following two journeys cover the server/client split of the detail page.
|
||||
* The sections are now React Server Components composed in the route and passed
|
||||
* through a client shell; these assert that the two halves still meet correctly
|
||||
* in a real browser, which no unit test can prove.
|
||||
*/
|
||||
|
||||
test('admissions year/trend toggle still switches views after the server/client split', async ({ page }) => {
|
||||
// Find a school with at least two years carrying an offer rate — the toggle
|
||||
// only appears then. Data-invariant: uses whatever the environment holds.
|
||||
const res = await page.request.get('/api/schools?search=primary&per_page=50');
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const candidates: number[] = ((await res.json()).schools ?? []).map((s: { urn: number }) => s.urn);
|
||||
|
||||
let target: number | null = null;
|
||||
for (const urn of candidates.slice(0, 12)) {
|
||||
const detail = await page.request.get(`/api/schools/${urn}`);
|
||||
if (!detail.ok()) continue;
|
||||
const history = (await detail.json()).admissions_history ?? [];
|
||||
const withRate = history.filter(
|
||||
(h: { first_preference_offer_pct?: number | null }) => h.first_preference_offer_pct != null,
|
||||
);
|
||||
if (withRate.length >= 2) { target = urn; break; }
|
||||
}
|
||||
test.skip(target === null, 'no school in this environment has 2+ years of admissions offer data');
|
||||
|
||||
await page.goto(`/school/${target}`);
|
||||
await expect(page.locator('#admissions')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const yearBtn = page.getByRole('button', { name: 'This year' });
|
||||
const trendBtn = page.getByRole('button', { name: /-year trend$/ });
|
||||
await expect(yearBtn).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// The toggle is the one client island inside an otherwise server-rendered
|
||||
// section: clicking it must swap the two server-rendered views.
|
||||
await trendBtn.click();
|
||||
await expect(trendBtn).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(yearBtn).toHaveAttribute('aria-pressed', 'false');
|
||||
|
||||
await yearBtn.click();
|
||||
await expect(yearBtn).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
|
||||
test('sticky section nav jumps to server-rendered sections', async ({ page }) => {
|
||||
const [urn] = await twoPrimaryUrns(page);
|
||||
await page.goto(`/school/${urn}`);
|
||||
await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// The nav is client-rendered from a server-computed list, while the sections
|
||||
// themselves are server-rendered. Every link must resolve to a real section:
|
||||
// the scroll-spy finds them with document.getElementById, so a mismatch
|
||||
// between the two halves would dead-end here.
|
||||
const navLinks = page.locator('nav a[href^="#"]');
|
||||
const count = await navLinks.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const href = await navLinks.nth(i).getAttribute('href');
|
||||
expect(href).toBeTruthy();
|
||||
await expect(page.locator(href!)).toHaveCount(1);
|
||||
}
|
||||
|
||||
// And following one actually moves the page.
|
||||
const before = await page.evaluate(() => window.scrollY);
|
||||
await navLinks.last().click();
|
||||
await page.waitForTimeout(600);
|
||||
const after = await page.evaluate(() => window.scrollY);
|
||||
expect(after).toBeGreaterThan(before);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user