← back to Govarbitrage
TK-00046 test: runtime regression guard for /agents sort + density
e80177224502ed5221614452a5bc4adea2f41838 · 2026-09-16 15:34:58 -0700 · Steve Abrams
The sort + density fix landed 2026-07-26 but nothing verified it kept
working. Source/bundle inspection only proves the code is deployed, not
that the controls render and are wired, so this asserts observable
behaviour in a real browser instead.
Covers: controls render; sort reorders across all four keys; the density
slider changes the grid's minmax() track; both prefs survive a reload.
Intercepts /api/agents/search with a fixture, so it never calls Google
Places and costs $0 (a real search bills ~$0.04/query x 3 lenses x kinds).
The fixture is ordered so best/name/reviews/rating each yield a different
sequence — a no-op sort cannot pass.
Verified negative-capable: with name-sort stubbed to return the list
unsorted, exactly the two name-dependent tests go red while the render
and density tests stay green.
Basic-auth creds come from the same BASIC_AUTH env src/middleware.ts
reads (nothing hardcoded); E2E_CHANNEL drives system Chrome when the
shared ms-playwright cache is lock-contended by another project.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
A tests/e2e/agents-sort-density.spec.ts
Diff
commit e80177224502ed5221614452a5bc4adea2f41838
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 16 15:34:58 2026 -0700
TK-00046 test: runtime regression guard for /agents sort + density
The sort + density fix landed 2026-07-26 but nothing verified it kept
working. Source/bundle inspection only proves the code is deployed, not
that the controls render and are wired, so this asserts observable
behaviour in a real browser instead.
Covers: controls render; sort reorders across all four keys; the density
slider changes the grid's minmax() track; both prefs survive a reload.
Intercepts /api/agents/search with a fixture, so it never calls Google
Places and costs $0 (a real search bills ~$0.04/query x 3 lenses x kinds).
The fixture is ordered so best/name/reviews/rating each yield a different
sequence — a no-op sort cannot pass.
Verified negative-capable: with name-sort stubbed to return the list
unsorted, exactly the two name-dependent tests go red while the render
and density tests stay green.
Basic-auth creds come from the same BASIC_AUTH env src/middleware.ts
reads (nothing hardcoded); E2E_CHANNEL drives system Chrome when the
shared ms-playwright cache is lock-contended by another project.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
tests/e2e/agents-sort-density.spec.ts | 138 ++++++++++++++++++++++++++++++++++
1 file changed, 138 insertions(+)
diff --git a/tests/e2e/agents-sort-density.spec.ts b/tests/e2e/agents-sort-density.spec.ts
new file mode 100644
index 0000000..cf55e75
--- /dev/null
+++ b/tests/e2e/agents-sort-density.spec.ts
@@ -0,0 +1,138 @@
+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);
+});
← bfb96b1 auto-data-snapshot: 2026-09-16T14:34:47 (1 data files) — tsc
·
back to Govarbitrage
·
chore: v0.5.3 (session close — TK-00046 /agents sort+density 51d02ee →