diff --git a/components/links.tsx b/components/links.tsx index 0482a64fc..a044d7926 100644 --- a/components/links.tsx +++ b/components/links.tsx @@ -4,6 +4,7 @@ import { BillTopic, CurrentCommittee } from "../functions/src/bills/types" import { Testimony } from "components/db/testimony" import { Bill, MemberContent } from "./db" import { formatBillId } from "./formatting" +import { billsDefaultIndex } from "./search/bills/useBillSort" import { TFunction } from "next-i18next" type LinkProps = PropsWithChildren< @@ -145,9 +146,10 @@ export const twitterShareLink = (publication: Testimony, t: TFunction) => { } export const billSearchByTopicLink = (court: number, topic: BillTopic) => { + // Routed uiState is keyed by the page's index name. const params = { - "bills/sort/latestTestimonyAt:desc[multiselectHierarchicalMenu][topics.lvl1][0]": `${topic.category} > ${topic.topic}`, - "bills/sort/latestTestimonyAt:desc[refinementList][court][0]": `${court}` + [`${billsDefaultIndex}[multiselectHierarchicalMenu][topics.lvl1][0]`]: `${topic.category} > ${topic.topic}`, + [`${billsDefaultIndex}[refinementList][court][0]`]: `${court}` } - return `/bills?${new URLSearchParams(params).toString()}` + return `${maple.billSearch()}?${new URLSearchParams(params).toString()}` } diff --git a/components/search/bills/BillSearch.tsx b/components/search/bills/BillSearch.tsx index 6ee7bdfb8..bfc49538b 100644 --- a/components/search/bills/BillSearch.tsx +++ b/components/search/bills/BillSearch.tsx @@ -23,14 +23,20 @@ import { BillHit } from "./BillHit" import { useBillRefinements } from "./useBillRefinements" import { SortBy, SortByWithConfigurationItem } from "../SortBy" import { getServerConfig, VirtualFilters } from "../common" -import { billsSearchParams } from "../searchParams" -import { useBillSort } from "./useBillSort" +import { billsRelevanceSort, billsSearchParams } from "../searchParams" +import { billsDefaultIndex, useBillSort } from "./useBillSort" import { FC, useState } from "react" import { pathToSearchState, searchStateToUrl } from "../routingHelpers" const searchClient = new TypesenseInstantSearchAdapter({ server: getServerConfig(), - additionalSearchParameters: billsSearchParams + // sort_by only reaches Typesense under the default option, whose index name + // (billsDefaultIndex) has no sort segment; every other option's + // "bills/sort/" index name overrides it in the adapter. + additionalSearchParameters: { + ...billsSearchParams, + sort_by: billsRelevanceSort + } }).searchClient const extractLastSegmentOfRefinements = (items: any[]) => { @@ -61,13 +67,12 @@ const extractLastSegmentOfRefinements = (items: any[]) => { export const BillSearch = () => { const items = useBillSort() - const initialSortByValue = items[0].value return ( ": the adapter only reads a + * sort from an index name that has a sort segment, and otherwise uses the + * sort_by BillSearch.tsx pins on the adapter. That keeps the long relevance + * clause out of Browse Bills URLs, where the index name is the qs key of the + * routed uiState (it would vanish entirely under InstantSearch's singleIndex + * state mapping, which the router does not use yet). BillSearch.tsx keys the + * page on it and links.tsx keys deep links on it. + */ +export const billsDefaultIndex = "bills" export const useBillSort = () => { const now = useRef(new Date().getTime()) @@ -12,12 +21,12 @@ export const useBillSort = () => { const items: SortByWithConfigurationItem[] = useMemo( () => [ { - label: t("sort_by.most_recent_testimony"), - value: "bills/sort/latestTestimonyAt:desc" + label: t("sort_by.relevance"), + value: billsDefaultIndex }, { - label: t("sort_by.relevance"), - value: `bills/sort/${billsRelevanceSort}` + label: t("sort_by.most_recent_testimony"), + value: "bills/sort/latestTestimonyAt:desc" }, { label: t("sort_by.testimony_count"), diff --git a/components/search/searchParams.ts b/components/search/searchParams.ts index fa3206471..dd08234ac 100644 --- a/components/search/searchParams.ts +++ b/components/search/searchParams.ts @@ -74,9 +74,10 @@ export const billsSearchParams = { exclude_fields: "body,numberVariants" } satisfies SearchParameters -/** The app's "Relevance" sort option (see useBillSort.tsx). The eval harness - * must use this sort; the UI's default latestTestimonyAt:desc sort does not - * measure text relevance. +/** The app's "Relevance" sort option and the Browse Bills default (see + * useBillSort.tsx; BillSearch.tsx pins it as the adapter's sort_by). The eval + * harness must use this sort; every other bills sort is a date or a count and + * does not measure text relevance. * * The `_eval` clause sorts every Order and Extension Order below every other * document. They are procedural — "Extension Order - Education", "Order @@ -98,11 +99,11 @@ export const billsSearchParams = { * legislationType refinement (useBillRefinements.tsx) is how a searcher who * wants them gets them back, which is why the facet ships with this sort. * - * Written as two `!=` clauses rather than the equivalent `!=[...]` list form: - * this string doubles as the InstantSearch index name, which travels through - * the URL as a qs key, and qs parses percent-encoded square brackets in a key - * as nesting — mangling the routed uiState so a reload or shared link loses - * the query, refinements and sort. No other character here is special to qs. + * Written as two `!=` clauses rather than the equivalent `!=[...]` list form. + * A sort embedded in an InstantSearch index name becomes a qs key in the URL, + * and qs parses percent-encoded square brackets in a key as nesting, mangling + * the routed uiState. This string no longer rides in the index name, but the + * hearings relevance sort does, so all three stay bracket-free. */ export const billsRelevanceSort = "_eval(legislationType:!=`Order` && legislationType:!=`Extension Order`):desc,_text_match:desc,testimonyCount:desc" @@ -129,8 +130,9 @@ export const testimonySearchParams = { exclude_fields: "billIdVariants" } satisfies SearchParameters -/** The app's "Relevance" sort option (see useTestimonySort in - * testimony/TestimonySearch.tsx), and the sort the eval harness must use. +/** The app's "Relevance" sort option and the Browse Testimony default (see + * useTestimonySort in testimony/TestimonySearch.tsx, which pins it as the + * adapter's sort_by), and the sort the eval harness must use. */ export const testimonyRelevanceSort = "_text_match:desc,publishedAt:desc" diff --git a/components/search/testimony/TestimonySearch.tsx b/components/search/testimony/TestimonySearch.tsx index 9c8e4e751..d1e7120f1 100644 --- a/components/search/testimony/TestimonySearch.tsx +++ b/components/search/testimony/TestimonySearch.tsx @@ -32,15 +32,33 @@ import { FollowContext, OrgFollowStatus } from "components/shared/FollowContext" import { pathToSearchState, searchStateToUrl } from "../routingHelpers" import { useTranslation } from "next-i18next" +/** The InstantSearch index name of the default (Relevance) sort: the bare + * collection, with the sort pinned on the adapter below, the same way Browse + * Bills does it (see billsDefaultIndex in bills/useBillSort.tsx). With no + * query every document's text match is equal, so the landing page falls + * through to the sort's publishedAt:desc tiebreak and reads newest-first. + */ +export const testimonyDefaultIndex = "publishedTestimony" + const searchClient = new TypesenseInstantSearchAdapter({ server: getServerConfig(), - additionalSearchParameters: testimonySearchParams + // sort_by only reaches Typesense under the default option, whose index name + // has no sort segment; the other options' "publishedTestimony/sort/" + // index names override it in the adapter. + additionalSearchParameters: { + ...testimonySearchParams, + sort_by: testimonyRelevanceSort + } }).searchClient export const useTestimonySort = () => { const { t } = useTranslation("search") const items: SortByItem[] = useMemo( () => [ + { + label: t("sort_by.relevance"), + value: testimonyDefaultIndex + }, { label: t("sort_by.newest"), value: "publishedTestimony/sort/publishedAt:desc" @@ -48,10 +66,6 @@ export const useTestimonySort = () => { { label: t("sort_by.oldest"), value: "publishedTestimony/sort/publishedAt:asc" - }, - { - label: t("sort_by.relevance"), - value: `publishedTestimony/sort/${testimonyRelevanceSort}` } ], [t] @@ -60,13 +74,12 @@ export const useTestimonySort = () => { } export const TestimonySearch = () => { - const initialSortByValue = useTestimonySort()[0].value return ( { }) // Array of sorting test configurations -// Need to add test for sort by relevant const sortingTests: string[] = [ + "Sort by Relevance", "Sort by Testimony Count", "Sort by Cosponsor Count", "Sort by Next Hearing Date", diff --git a/tests/e2e/page_objects/testimony.ts b/tests/e2e/page_objects/testimony.ts index 02baca3f1..5d0bcd7b3 100644 --- a/tests/e2e/page_objects/testimony.ts +++ b/tests/e2e/page_objects/testimony.ts @@ -38,7 +38,6 @@ export class TestimonyPage { } async sort(option: string) { - // previoud code: await this.page.getByText("Sort by New -> Old").click() await this.page .getByText(/Sort by/i) .first() diff --git a/tests/e2e/testimony.spec.ts b/tests/e2e/testimony.spec.ts index 94a1c56ea..c9c871a0d 100644 --- a/tests/e2e/testimony.spec.ts +++ b/tests/e2e/testimony.spec.ts @@ -163,18 +163,19 @@ test.describe("Testimony Filtering", () => { }) }) -test.describe("Testimony Sorting", () => { - test("should sort by new -> old", async ({ page }) => { - const testimonyPage = new TestimonyPage(page) - await testimonyPage.sort("Sort by New -> Old") - const sortValue = page.getByText("Sort by New -> Old", { exact: true }) - await expect(sortValue).toBeVisible() - }) +const sortingTests: string[] = [ + "Sort by Relevance", + "Sort by Newest", + "Sort by Oldest" +] - test("should sort by old -> new", async ({ page }) => { - const testimonyPage = new TestimonyPage(page) - await testimonyPage.sort("Sort by Old -> New") - const sortValue = page.getByText("Sort by Old -> New", { exact: true }) - await expect(sortValue).toBeVisible() - }) +test.describe("Testimony Sorting", () => { + for (const option of sortingTests) { + test(`should sort testimony by ${option}`, async ({ page }) => { + const testimonyPage = new TestimonyPage(page) + await testimonyPage.sort(option) + const sortValue = page.getByText(option, { exact: true }) + await expect(sortValue).toBeVisible() + }) + } })