|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import { test, expect, Page } from '@playwright/test';
|
|
|
|
|
import { test, expect, Locator, Page } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Journey tests for SchoolCompare, run against the staging environment as the
|
|
|
|
@@ -19,6 +19,31 @@ function schoolLinks(page: Page) {
|
|
|
|
|
return page.locator('a[href^="/school/"]');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A scroll offset that has stopped moving.
|
|
|
|
|
*
|
|
|
|
|
* The carousel arrows scroll with `behavior: 'smooth'`, so a reading taken
|
|
|
|
|
* straight after a click lands mid-animation. Measured against staging: the
|
|
|
|
|
* animation runs ~700ms, and a poll for "has it moved at all" is satisfied
|
|
|
|
|
* 50ms in, at 13px of a 1300px journey. A test that then records an offset,
|
|
|
|
|
* does something, and records again is measuring the tail of the arrow's
|
|
|
|
|
* animation rather than the effect of whatever it did in between.
|
|
|
|
|
*
|
|
|
|
|
* Two identical readings in a row is the cheapest sound definition of settled.
|
|
|
|
|
*/
|
|
|
|
|
async function settledScrollLeft(scroller: Locator): Promise<number> {
|
|
|
|
|
let previous = -1;
|
|
|
|
|
await expect
|
|
|
|
|
.poll(async () => {
|
|
|
|
|
const current = await scroller.evaluate((node: HTMLElement) => Math.round(node.scrollLeft));
|
|
|
|
|
const settled = current === previous;
|
|
|
|
|
previous = current;
|
|
|
|
|
return settled;
|
|
|
|
|
}, { timeout: 10_000 })
|
|
|
|
|
.toBe(true);
|
|
|
|
|
return previous;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Two URNs guaranteed to be pure-primary (same phase). The compare page's
|
|
|
|
|
* phase tabs split all-through schools (which carry KS4 data) onto the
|
|
|
|
@@ -2776,9 +2801,7 @@ test('nearby schools link on to other schools and into compare', async ({ page }
|
|
|
|
|
await expect(back).toBeDisabled();
|
|
|
|
|
|
|
|
|
|
await forward.click();
|
|
|
|
|
await expect
|
|
|
|
|
.poll(() => scroller.evaluate((node: HTMLElement) => node.scrollLeft))
|
|
|
|
|
.toBeGreaterThan(8);
|
|
|
|
|
expect(await settledScrollLeft(scroller)).toBeGreaterThan(8);
|
|
|
|
|
await expect(back).toBeEnabled();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2794,15 +2817,61 @@ test('nearby schools link on to other schools and into compare', async ({ page }
|
|
|
|
|
//
|
|
|
|
|
// A few pixels of snap or sub-pixel adjustment are fine; a reset to the
|
|
|
|
|
// start is not, which is the whole point of the check.
|
|
|
|
|
const offsetBefore = await scroller.evaluate((node: HTMLElement) => node.scrollLeft);
|
|
|
|
|
const offsetBefore = await settledScrollLeft(scroller);
|
|
|
|
|
await section.getByRole('button', { name: /Add to compare/ }).last().click();
|
|
|
|
|
await expect(
|
|
|
|
|
section.getByRole('button', { name: /Added to compare/ }).first(),
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
const offsetAfter = await scroller.evaluate((node: HTMLElement) => node.scrollLeft);
|
|
|
|
|
const offsetAfter = await settledScrollLeft(scroller);
|
|
|
|
|
expect(Math.abs(offsetAfter - offsetBefore)).toBeLessThanOrEqual(8);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Every section in the mobile jump sheet can actually be reached.
|
|
|
|
|
*
|
|
|
|
|
* The sheet is a fixed bottom sheet, and the app has a fixed bottom tab bar.
|
|
|
|
|
* `position: sticky` with a z-index on the sticky nav makes it a stacking
|
|
|
|
|
* context, so the sheet's own z-index orders it only within that context —
|
|
|
|
|
* against the tab bar, the nav's value is what counts. The last item in the
|
|
|
|
|
* sheet was therefore painted over and untappable as soon as the list grew
|
|
|
|
|
* long enough to reach the bar, which adding "Nearby schools" is what did.
|
|
|
|
|
*
|
|
|
|
|
* Bounding boxes are not enough to catch this: the item is in the viewport and
|
|
|
|
|
* the right size, it is simply underneath something. So this asks the question
|
|
|
|
|
* a thumb asks — what is on top at this point.
|
|
|
|
|
*/
|
|
|
|
|
test('every section in the mobile jump sheet is tappable, not under the tab bar', async ({ page }) => {
|
|
|
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
|
|
|
await searchByName(page, 'Primary');
|
|
|
|
|
await schoolLinks(page).first().click();
|
|
|
|
|
await page.waitForURL(/\/school\//);
|
|
|
|
|
|
|
|
|
|
// Scroll down so the sticky nav is docked and the sheet has somewhere to open.
|
|
|
|
|
await page.evaluate(() => window.scrollTo({ top: 1200 }));
|
|
|
|
|
// Two controls carry aria-haspopup: the mobile "Section" button and the
|
|
|
|
|
// desktop "All" one, which is display:none here but still in the DOM.
|
|
|
|
|
await page.locator('[aria-haspopup="menu"]:visible').click();
|
|
|
|
|
|
|
|
|
|
const sheet = page.locator('[role="menu"]');
|
|
|
|
|
await expect(sheet).toBeVisible();
|
|
|
|
|
|
|
|
|
|
const covered = await sheet.evaluate((panel: HTMLElement) =>
|
|
|
|
|
Array.from(panel.querySelectorAll('[role="menuitem"]'))
|
|
|
|
|
.map((el) => {
|
|
|
|
|
const box = el.getBoundingClientRect();
|
|
|
|
|
const hit = document.elementFromPoint(
|
|
|
|
|
Math.round(box.left + box.width / 2),
|
|
|
|
|
Math.round(box.top + box.height / 2),
|
|
|
|
|
);
|
|
|
|
|
return { label: (el as HTMLElement).innerText.trim().replace(/\s+/g, ' '), reachable: !!(hit && hit.closest('[role="menuitem"]')) };
|
|
|
|
|
})
|
|
|
|
|
.filter((item) => !item.reachable)
|
|
|
|
|
.map((item) => item.label),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(covered).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The nearby-schools section at MOBILE.md's three reference widths.
|
|
|
|
|
*
|
|
|
|
|