From 8c62edd6b8f3fb9f31d8875edb33df2db2166129 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:00:32 +0800 Subject: [PATCH 1/4] Improve Therapy best-match visibility --- .../therapy-compass/screens/search-screen.tsx | 8 +++- .../therapy-compass/therapy-card.tsx | 14 ++++++- tests/recommend-screen.dom.test.tsx | 3 +- ...herapy-compass-search-filters.dom.test.tsx | 41 ++++++++++++++++++- tests/ui-route-coverage.spec.ts | 14 +++++-- 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/components/therapy-compass/screens/search-screen.tsx b/src/components/therapy-compass/screens/search-screen.tsx index cc487c3767..f692112b3f 100644 --- a/src/components/therapy-compass/screens/search-screen.tsx +++ b/src/components/therapy-compass/screens/search-screen.tsx @@ -36,6 +36,10 @@ export function SearchScreen() { const q = b.search.query; const results = b.searchResults; const shown = results.slice(0, MAX_CARDS); + // An empty query gives every record the same baseline score and then sorts + // alphabetically, so only call out a best match when the scorer had real + // query evidence to rank. Facets narrow the list but do not rank it. + const hasRankedQuery = q.trim().length > 0; const topics = useMemo(() => new Set(b.search.tags), [b.search.tags]); // Filters only — the query is deliberately excluded. It counts toward the // sheet's Clear all (see below), because `clearSearch` resets it, but the @@ -236,8 +240,8 @@ export function SearchScreen() { /> ) : (
- {shown.map((t) => ( - + {shown.map((t, index) => ( + ))}
)} diff --git a/src/components/therapy-compass/therapy-card.tsx b/src/components/therapy-compass/therapy-card.tsx index be5e2e5b62..d7f342e5de 100644 --- a/src/components/therapy-compass/therapy-card.tsx +++ b/src/components/therapy-compass/therapy-card.tsx @@ -8,6 +8,7 @@ import { FileText, Heart, Scale, + Sparkles, Target, TriangleAlert, type LucideIcon, @@ -26,8 +27,8 @@ import { useTherapyFavourite } from "./use-therapy-favourite"; /** * Search-result cards spend all three actions as `secondary` so the page keeps - * the one filled `--command` slot (COMPONENTS.md §9.1). Recommend's featured - * top match is that page-level primary, so only `featured` promotes Open. + * the one filled `--command` slot (COMPONENTS.md §9.1). A featured top match + * is that page-level primary, so only `featured` promotes Open. */ const cardActionButton = "min-w-0 px-2 text-xs sm:px-4 sm:text-sm-minus"; @@ -108,6 +109,15 @@ export function ResultCard({
+ {featured ? ( +
+
+ ) : null}
{rank != null ? ( diff --git a/tests/recommend-screen.dom.test.tsx b/tests/recommend-screen.dom.test.tsx index c1a74bd199..8a724c944a 100644 --- a/tests/recommend-screen.dom.test.tsx +++ b/tests/recommend-screen.dom.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import type { Therapy } from "@/components/therapy-compass/data/types"; @@ -83,6 +83,7 @@ describe("Recommend screen", () => { const cards = document.querySelectorAll("[data-therapy-result-card]"); expect(cards).toHaveLength(2); expect(cards[0]).toHaveAttribute("data-therapy-result-featured"); + expect(within(cards[0] as HTMLElement).getByText("Best match")).toBeInTheDocument(); const featuredOpen = cards[0]?.querySelector('[aria-label="Open record"]'); expect(featuredOpen).toHaveClass("bg-[color:var(--command)]"); expect(screen.getAllByText("WHY MATCHED").length).toBeGreaterThan(0); diff --git a/tests/therapy-compass-search-filters.dom.test.tsx b/tests/therapy-compass-search-filters.dom.test.tsx index 6a2f6de792..83ade90c83 100644 --- a/tests/therapy-compass-search-filters.dom.test.tsx +++ b/tests/therapy-compass-search-filters.dom.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -50,6 +50,18 @@ vi.mock("@/components/therapy-compass/bindings", () => ({ toggleBriefOnly, clearSearch, clearSearchFilters, + isInCompare: () => false, + open: vi.fn(), + openSheet: vi.fn(), + toggleCompare: vi.fn(), + }), +})); + +vi.mock("@/components/account-data-provider", () => ({ + useAccountData: () => ({ + isAuthenticated: false, + isSaved: () => false, + setFavourite: vi.fn(), }), })); @@ -183,4 +195,31 @@ describe("therapy-compass search filter contract adoption", () => { await user.click(screen.getByTestId("therapy-filter-trigger-phone")); expect(screen.queryByTestId("therapy-filter-panel-clear")).not.toBeInTheDocument(); }); + + it("visually identifies only the first ranked result as the best match", () => { + bindingsState.query = "anxiety"; + bindingsState.queryMatches = [therapy()]; + bindingsState.searchResults = [ + therapy({ slug: "first", name: "First ranked therapy" }), + therapy({ slug: "second", name: "Second ranked therapy" }), + ]; + + render(); + + const cards = document.querySelectorAll("[data-therapy-result-card]"); + expect(cards).toHaveLength(2); + expect(cards[0]).toHaveAttribute("data-therapy-result-featured"); + expect(cards[1]).not.toHaveAttribute("data-therapy-result-featured"); + expect(within(cards[0] as HTMLElement).getByText("Best match")).toBeInTheDocument(); + expect(within(cards[1] as HTMLElement).queryByText("Best match")).not.toBeInTheDocument(); + }); + + it("does not imply a best match when the unqueried catalogue is alphabetical", () => { + bindingsState.searchResults = [therapy({ slug: "first" })]; + + render(); + + expect(document.querySelector("[data-therapy-result-featured]")).toBeNull(); + expect(screen.queryByText("Best match")).not.toBeInTheDocument(); + }); }); diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts index 57703d9c27..c119ecb01f 100644 --- a/tests/ui-route-coverage.spec.ts +++ b/tests/ui-route-coverage.spec.ts @@ -327,6 +327,9 @@ test.describe("previously uncovered production routes", () => { const card = page.locator("[data-therapy-result-card]").first(); await expect(card).toBeVisible({ timeout: 30_000 }); + await expect(card).toHaveAttribute("data-therapy-result-featured", ""); + await expect(card.getByText("Best match", { exact: true })).toBeVisible(); + await expect(page.locator("[data-therapy-result-highlight]")).toHaveCount(1); for (const width of [320, 390, 639, 768, 1440, 1920]) { await page.setViewportSize({ width, height: width < 768 ? 844 : 900 }); @@ -334,6 +337,7 @@ test.describe("previously uncovered production routes", () => { const layout = await card.evaluate((element) => { const bounds = element.getBoundingClientRect(); + const borderLeft = Number.parseFloat(getComputedStyle(element).borderLeftWidth); const copy = element.querySelector("[data-therapy-result-copy]")!.getBoundingClientRect(); const evidence = element.querySelector("[data-therapy-result-evidence]")!.getBoundingClientRect(); const actions = element.querySelector("[data-therapy-result-actions]")!; @@ -349,6 +353,7 @@ test.describe("previously uncovered production routes", () => { }); return { card: { left: bounds.left, right: bounds.right }, + borderLeft, copyLeft: copy.left, evidence: { left: evidence.left, right: evidence.right }, buttons, @@ -356,9 +361,12 @@ test.describe("previously uncovered production routes", () => { }); if (width < 640) { - expect(Math.abs(layout.evidence.left - layout.card.left), `${width}px evidence left edge`).toBeLessThanOrEqual( - 1, - ); + // Featured cards carry the intentional 3px best-match accent edge. + // The evidence panel remains full-bleed inside that border. + expect( + Math.abs(layout.evidence.left - (layout.card.left + layout.borderLeft)), + `${width}px evidence left edge`, + ).toBeLessThanOrEqual(1); expect( Math.abs(layout.card.right - layout.evidence.right), `${width}px evidence right edge`, From 70b981def097a7ea6f3fd132b92a98288d2ceed0 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:14:20 +0800 Subject: [PATCH 2/4] test: exclude punctuation-only Therapy search from best match --- tests/therapy-compass-search-filters.dom.test.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/therapy-compass-search-filters.dom.test.tsx b/tests/therapy-compass-search-filters.dom.test.tsx index 83ade90c83..4d6b70c795 100644 --- a/tests/therapy-compass-search-filters.dom.test.tsx +++ b/tests/therapy-compass-search-filters.dom.test.tsx @@ -222,4 +222,15 @@ describe("therapy-compass search filter contract adoption", () => { expect(document.querySelector("[data-therapy-result-featured]")).toBeNull(); expect(screen.queryByText("Best match")).not.toBeInTheDocument(); }); + + it("does not imply a best match for a punctuation-only query", () => { + bindingsState.query = "!!!"; + bindingsState.queryMatches = [therapy({ slug: "first" }), therapy({ slug: "second" })]; + bindingsState.searchResults = [therapy({ slug: "first" }), therapy({ slug: "second" })]; + + render(); + + expect(document.querySelector("[data-therapy-result-featured]")).toBeNull(); + expect(screen.queryByText("Best match")).not.toBeInTheDocument(); + }); }); From 2b93f50fb93c8c34a88f3d0d8f894b4e4c0fc857 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:26:26 +0800 Subject: [PATCH 3/4] fix: share therapy query validity with ranking --- src/lib/therapy-ranking.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/therapy-ranking.ts b/src/lib/therapy-ranking.ts index 0b2ce340b2..93ac8a6c94 100644 --- a/src/lib/therapy-ranking.ts +++ b/src/lib/therapy-ranking.ts @@ -16,6 +16,11 @@ const normalize = (value: string | null | undefined) => .replace(/\s+/g, " ") .trim(); +/** True only when the scorer can derive at least one searchable token. */ +export function hasSearchableTherapyQuery(query: string | null | undefined): boolean { + return Boolean(normalize(query)); +} + /** * The single Therapy ranking contract used by both the dedicated catalogue and * universal discovery. Keeping one scorer prevents a search handoff from From d149aad7215af2f5c33e770a1ef771f4f8982ff8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:26:28 +0800 Subject: [PATCH 4/4] fix: only feature therapy results for searchable queries --- src/components/therapy-compass/screens/search-screen.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/therapy-compass/screens/search-screen.tsx b/src/components/therapy-compass/screens/search-screen.tsx index f692112b3f..606384b5f1 100644 --- a/src/components/therapy-compass/screens/search-screen.tsx +++ b/src/components/therapy-compass/screens/search-screen.tsx @@ -19,6 +19,7 @@ import { pageContainer } from "@/components/ui-primitives"; import { useTcBindings } from "../bindings"; import { matchesAvailability, matchesTopics } from "../data/select"; +import { hasSearchableTherapyQuery } from "@/lib/therapy-ranking"; import { LoadingState } from "../ui"; import { ResultCard } from "../therapy-card"; import { TherapyReviewNotice } from "../therapy-review-notice"; @@ -39,7 +40,7 @@ export function SearchScreen() { // An empty query gives every record the same baseline score and then sorts // alphabetically, so only call out a best match when the scorer had real // query evidence to rank. Facets narrow the list but do not rank it. - const hasRankedQuery = q.trim().length > 0; + const hasRankedQuery = hasSearchableTherapyQuery(q); const topics = useMemo(() => new Set(b.search.tags), [b.search.tags]); // Filters only — the query is deliberately excluded. It counts toward the // sheet's Clear all (see below), because `clearSearch` resets it, but the