← back to Govarbitrage
tests/e2e/agents-sort-density.spec.ts
139 lines
import { test, expect, type Page } from "@playwright/test";
// TK-00046 regression guard — the /agents results grid must keep its sort +
// density controls (Steve standing rule: every product/results grid gets sort +
// density, persisted to localStorage).
//
// The real search bills Google Places (~$0.04/query x 3 lenses x kinds), so the
// API is intercepted with a fixture: this spec costs $0 and never calls Places.
//
// This is a NEGATIVE-CAPABLE test: it asserts observable runtime behaviour
// (order actually changes, the grid's minmax actually changes, prefs actually
// survive a reload) rather than the mere presence of markup — so stripping the
// controls, or leaving them rendered but unwired, both turn it RED.
// The app fronts everything with a Basic-Auth wall (src/middleware.ts), and
// clearing that wall IS the login — presentedAdminBasicAuth() mints an ADMIN
// session, so no email form is involved. Sending the creds on each request
// exercises the REAL gated path instead of disabling the wall for the test.
// Creds come from the same BASIC_AUTH env the middleware reads, so nothing is
// hardcoded here; skip if the wall is explicitly disabled.
const [BA_USER, BA_PASS] = (process.env.BASIC_AUTH ?? "admin:DW2024!").split(":");
if (BA_USER) test.use({ httpCredentials: { username: BA_USER, password: BA_PASS ?? "" } });
// E2E_CHANNEL=chrome drives the system-installed Google Chrome instead of
// Playwright's bundled chromium. Useful on a box where the shared
// ~/Library/Caches/ms-playwright dir is lock-contended by another project's
// install, and it exercises a real browser rather than the headless shell.
if (process.env.E2E_CHANNEL) test.use({ channel: process.env.E2E_CHANNEL });
const FIXTURE = {
location: "Austin, TX",
kinds: ["buyer", "leasing"],
queries: 3,
results: [
// Deliberately ordered so "best" (server order), "name" and "reviews" each
// produce a DIFFERENT sequence — otherwise a broken sort could still pass.
{ id: "c", name: "Cobalt Commercial", address: "3 C St", rating: 4.1, reviews: 300,
phone: null, website: null, mapsUrl: null, primaryType: "real_estate_agency", kinds: ["buyer"] },
{ id: "a", name: "Apex Realty Partners", address: "1 A St", rating: 4.9, reviews: 12,
phone: null, website: null, mapsUrl: null, primaryType: "real_estate_agency", kinds: ["leasing"] },
{ id: "b", name: "Beacon Brokerage", address: "2 B St", rating: 3.2, reviews: 150,
phone: null, website: null, mapsUrl: null, primaryType: "real_estate_agency", kinds: ["buyer"] },
],
};
const NAMES = FIXTURE.results.map((r) => r.name);
async function search(page: Page) {
await page.route("**/api/agents/search**", (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(FIXTURE) }),
);
await page.goto("/agents");
await page.getByLabel(/city/i).first().fill("Austin");
await page.getByRole("button", { name: /find agents|search/i }).first().click();
await expect(page.getByText(/3 commercial agents/i).first()).toBeVisible();
}
/** Visible order of the result cards, by fixture name. */
async function cardOrder(page: Page): Promise<string[]> {
const body = await page.locator("body").innerText();
return NAMES.map((n) => ({ n, i: body.indexOf(n) }))
.filter((x) => x.i >= 0)
.sort((a, b) => a.i - b.i)
.map((x) => x.n);
}
/** The results grid's live minmax() track size, in px. */
async function gridMin(page: Page): Promise<number> {
const grid = page.locator('div.grid[style*="minmax"]').last();
const style = (await grid.getAttribute("style")) ?? "";
const m = style.match(/minmax\((\d+)px/);
expect(m, `grid style should carry a minmax() px track, got: ${style}`).not.toBeNull();
return Number(m![1]);
}
test("results grid exposes sort + density controls", async ({ page }) => {
await search(page);
await expect(page.getByLabel("Sort results")).toBeVisible();
await expect(page.getByLabel("Grid density")).toBeVisible();
});
test("sort actually reorders the results", async ({ page }) => {
await search(page);
// "best" = server order, untouched.
expect(await cardOrder(page)).toEqual(["Cobalt Commercial", "Apex Realty Partners", "Beacon Brokerage"]);
await page.getByLabel("Sort results").selectOption("name");
expect(await cardOrder(page)).toEqual(["Apex Realty Partners", "Beacon Brokerage", "Cobalt Commercial"]);
await page.getByLabel("Sort results").selectOption("reviews");
expect(await cardOrder(page)).toEqual(["Cobalt Commercial", "Beacon Brokerage", "Apex Realty Partners"]);
await page.getByLabel("Sort results").selectOption("rating");
expect(await cardOrder(page)).toEqual(["Apex Realty Partners", "Cobalt Commercial", "Beacon Brokerage"]);
});
test("density slider actually changes the grid track size", async ({ page }) => {
await search(page);
const before = await gridMin(page);
// Slider is inverted (right = denser = smaller cards); drive it to its min,
// then its max, and require the grid to move in both directions.
const slider = page.getByLabel("Grid density");
await slider.fill("220");
const wide = await gridMin(page);
await slider.fill("440");
const dense = await gridMin(page);
// Right = denser, so the max slider position must yield the SMALLER track.
expect(dense).toBeLessThan(wide);
// And the default must sit inside the control's range (not a frozen constant).
expect(before).toBeGreaterThanOrEqual(dense);
expect(before).toBeLessThanOrEqual(wide);
});
test("sort + density survive a reload (localStorage-persisted)", async ({ page }) => {
await search(page);
await page.getByLabel("Sort results").selectOption("name");
await page.getByLabel("Grid density").fill("440");
const densePx = await gridMin(page);
await page.reload();
// The controls only render once a search has returned >1 result, so re-run the
// search first — the persisted prefs are hydrated from localStorage on mount
// and must already be in force when the grid reappears.
await page.getByLabel(/city/i).first().fill("Austin");
await page.getByRole("button", { name: /find agents|search/i }).first().click();
await expect(page.getByText(/3 commercial agents/i).first()).toBeVisible();
// Both the control's own state AND the rendered output must reflect the
// restored prefs — order alone could coincide, and a restored <select> value
// that didn't drive the grid would be a silent half-fix.
await expect(page.getByLabel("Sort results")).toHaveValue("name");
expect(await cardOrder(page)).toEqual(["Apex Realty Partners", "Beacon Brokerage", "Cobalt Commercial"]);
expect(await gridMin(page)).toBe(densePx);
});