-
-
-
-
-
+
);
}
-
-function convertSearchInputArraysToSets(
- searchInputs: QueryParamData,
-): QueryParamData {
- return {
- ...searchInputs,
- status: new Set(searchInputs.status || []),
- fundingInstrument: new Set(searchInputs.fundingInstrument || []),
- eligibility: new Set(searchInputs.eligibility || []),
- agency: new Set(searchInputs.agency || []),
- category: new Set(searchInputs.category || []),
- };
-}
-
-function isValidJSON(str: string) {
- try {
- JSON.parse(str);
- return true;
- } catch (e) {
- return false; // String is not valid JSON
- }
-}
-
-function getParsedError() {
- return {
- type: "NetworkError",
- searchInputs: {
- status: new Set(),
- fundingInstrument: new Set(),
- eligibility: new Set(),
- agency: new Set(),
- category: new Set(),
- sortby: null,
- page: 1,
- actionType: "initialLoad",
- },
- message: "Invalid JSON returned",
- status: -1,
- } as ParsedError;
-}
diff --git a/frontend/src/app/[locale]/search/layout.tsx b/frontend/src/app/[locale]/search/layout.tsx
new file mode 100644
index 000000000..ceadc396b
--- /dev/null
+++ b/frontend/src/app/[locale]/search/layout.tsx
@@ -0,0 +1,27 @@
+import { SEARCH_CRUMBS } from "src/constants/breadcrumbs";
+
+import { useTranslations } from "next-intl";
+import { unstable_setRequestLocale } from "next-intl/server";
+
+import BetaAlert from "src/components/BetaAlert";
+import Breadcrumbs from "src/components/Breadcrumbs";
+import PageSEO from "src/components/PageSEO";
+import SearchCallToAction from "src/components/search/SearchCallToAction";
+
+export default function SearchLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ unstable_setRequestLocale("en");
+ const t = useTranslations("Search");
+ return (
+ <>
+
+
+
+
+ {children}
+ >
+ );
+}
diff --git a/frontend/src/app/[locale]/search/page.tsx b/frontend/src/app/[locale]/search/page.tsx
index 4e09b2b6c..453a70a82 100644
--- a/frontend/src/app/[locale]/search/page.tsx
+++ b/frontend/src/app/[locale]/search/page.tsx
@@ -1,27 +1,16 @@
import { Metadata } from "next";
-import Loading from "src/app/[locale]/search/loading";
-import QueryProvider from "src/app/[locale]/search/QueryProvider";
-import { SEARCH_CRUMBS } from "src/constants/breadcrumbs";
import withFeatureFlag from "src/hoc/search/withFeatureFlag";
import { Breakpoints } from "src/types/uiTypes";
import { convertSearchParamsToProperTypes } from "src/utils/search/convertSearchParamsToProperTypes";
import { useTranslations } from "next-intl";
import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
-import { Suspense } from "react";
-import BetaAlert from "src/components/BetaAlert";
-import Breadcrumbs from "src/components/Breadcrumbs";
import ContentDisplayToggle from "src/components/ContentDisplayToggle";
-import PageSEO from "src/components/PageSEO";
import SearchBar from "src/components/search/SearchBar";
-import SearchCallToAction from "src/components/search/SearchCallToAction";
import SearchFilters from "src/components/search/SearchFilters";
-import SearchPagination from "src/components/search/SearchPagination";
-import SearchPaginationFetch from "src/components/search/SearchPaginationFetch";
-import SearchResultsHeader from "src/components/search/SearchResultsHeader";
-import SearchResultsHeaderFetch from "src/components/search/SearchResultsHeaderFetch";
-import SearchResultsListFetch from "src/components/search/SearchResultsListFetch";
+import SearchResults from "src/components/search/SearchResults";
+import QueryProvider from "./QueryProvider";
export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
@@ -48,30 +37,15 @@ function Search({ searchParams }: { searchParams: searchParamsTypes }) {
unstable_setRequestLocale("en");
const t = useTranslations("Search");
const convertedSearchParams = convertSearchParamsToProperTypes(searchParams);
- const {
- agency,
- category,
- eligibility,
- fundingInstrument,
- page,
- query,
- sortby,
- status,
- } = convertedSearchParams;
+ const { agency, category, eligibility, fundingInstrument, query, status } =
+ convertedSearchParams;
if (!("page" in searchParams)) {
searchParams.page = "1";
}
- const key = Object.entries(searchParams).join(",");
- const pager1key = Object.entries(searchParams).join("-") + "pager1";
- const pager2key = Object.entries(searchParams).join("-") + "pager2";
return (
<>
-
-
-
-
@@ -94,58 +68,11 @@ function Search({ searchParams }: { searchParams: searchParamsTypes }) {
-
- }
- >
-
-
-
-
- }
- >
-
-
- }
- >
-
-
-
- }
- >
-
-
-
+
diff --git a/frontend/src/app/api/BaseApi.ts b/frontend/src/app/api/BaseApi.ts
index a9f75827e..3ab619432 100644
--- a/frontend/src/app/api/BaseApi.ts
+++ b/frontend/src/app/api/BaseApi.ts
@@ -117,12 +117,12 @@ export default abstract class BaseApi {
// or parsing the response.
throw fetchErrorToNetworkError(error, queryParamData);
}
+ if (!response.ok) {
+ handleNotOkResponse(responseBody, url, queryParamData);
+ }
const { data, message, pagination_info, status_code, warnings } =
responseBody;
- if (!response.ok) {
- handleNotOkResponse(responseBody, message, status_code, queryParamData);
- }
return {
data,
@@ -183,7 +183,7 @@ function createRequestBody(payload?: JSONRequestBody): XMLHttpRequestBodyInit {
/**
* Handle request errors
*/
-export function fetchErrorToNetworkError(
+function fetchErrorToNetworkError(
error: unknown,
searchInputs?: QueryParamData,
) {
@@ -197,29 +197,32 @@ export function fetchErrorToNetworkError(
function handleNotOkResponse(
response: APIResponse,
- message: string,
- status_code: number,
+ url: string,
searchInputs?: QueryParamData,
) {
const { errors } = response;
if (isEmpty(errors)) {
// No detailed errors provided, throw generic error based on status code
- throwError(message, status_code, searchInputs);
+ throwError(response, url, searchInputs);
} else {
if (errors) {
const firstError = errors[0] as APIResponseError;
- throwError(message, status_code, searchInputs, firstError);
+ throwError(response, url, searchInputs, firstError);
}
}
}
const throwError = (
- message: string,
- status_code: number,
+ response: APIResponse,
+ url: string,
searchInputs?: QueryParamData,
firstError?: APIResponseError,
) => {
- console.error("Throwing error: ", message, status_code, searchInputs);
+ const { status_code, message } = response;
+ console.error(
+ `API request error at ${url} (${status_code}): ${message}`,
+ searchInputs,
+ );
// Include just firstError for now, we can expand this
// If we need ValidationErrors to be more expanded
diff --git a/frontend/src/components/search/SearchFilters.tsx b/frontend/src/components/search/SearchFilters.tsx
index 8f2c3cf34..19baadc52 100644
--- a/frontend/src/components/search/SearchFilters.tsx
+++ b/frontend/src/components/search/SearchFilters.tsx
@@ -1,5 +1,4 @@
import { useTranslations } from "next-intl";
-import { unstable_setRequestLocale } from "next-intl/server";
import SearchFilterAccordion from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";
import {
@@ -23,7 +22,6 @@ export default function SearchFilters({
category: Set
;
opportunityStatus: Set;
}) {
- unstable_setRequestLocale("en");
const t = useTranslations("Search");
return (
diff --git a/frontend/src/components/search/SearchResults.tsx b/frontend/src/components/search/SearchResults.tsx
new file mode 100644
index 000000000..eba5d2bd8
--- /dev/null
+++ b/frontend/src/components/search/SearchResults.tsx
@@ -0,0 +1,61 @@
+import Loading from "src/app/[locale]/search/loading";
+import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher";
+
+import { Suspense } from "react";
+
+import SearchPagination from "src/components/search/SearchPagination";
+import SearchPaginationFetch from "src/components/search/SearchPaginationFetch";
+import SearchResultsHeader from "src/components/search/SearchResultsHeader";
+import SearchResultsHeaderFetch from "src/components/search/SearchResultsHeaderFetch";
+import SearchResultsListFetch from "src/components/search/SearchResultsListFetch";
+
+export default function SearchResults({
+ searchParams,
+ query,
+ loadingMessage,
+}: {
+ searchParams: QueryParamData;
+ query?: string | null;
+ loadingMessage: string;
+}) {
+ const { page, sortby } = searchParams;
+
+ const key = Object.entries(searchParams).join(",");
+ const pager1key = Object.entries(searchParams).join("-") + "pager1";
+ const pager2key = Object.entries(searchParams).join("-") + "pager2";
+ return (
+ <>
+ }
+ >
+
+
+
+
+ }
+ >
+
+
+ }>
+
+
+
+ }
+ >
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/search/error/SearchErrorAlert.tsx b/frontend/src/components/search/error/SearchErrorAlert.tsx
index e03f3a053..ebaec4e90 100644
--- a/frontend/src/components/search/error/SearchErrorAlert.tsx
+++ b/frontend/src/components/search/error/SearchErrorAlert.tsx
@@ -1,12 +1,9 @@
+import { Alert } from "@trussworks/react-uswds";
+
const SearchErrorAlert = () => (
-
-
-
We're sorry.
-
- There seems to have been an error. Please try your search again.
-
-
-
+
+ There seems to have been an error. Please try your search again.
+
);
export default SearchErrorAlert;
diff --git a/frontend/tests/components/search/SearchResults.test.tsx b/frontend/tests/components/search/SearchResults.test.tsx
new file mode 100644
index 000000000..a0a7d1d85
--- /dev/null
+++ b/frontend/tests/components/search/SearchResults.test.tsx
@@ -0,0 +1,58 @@
+import { render, screen } from "@testing-library/react";
+import { identity } from "lodash";
+import { useTranslationsMock } from "tests/utils/intlMocks";
+
+import SearchResults from "src/components/search/SearchResults";
+
+const mockUpdateQueryParams = jest.fn();
+
+jest.mock("src/hooks/useSearchParamUpdater", () => ({
+ useSearchParamUpdater: () => ({
+ updateQueryParams: mockUpdateQueryParams,
+ }),
+}));
+
+jest.mock("next-intl/server", () => ({
+ getTranslations: () => identity,
+ unstable_setRequestLocale: identity,
+}));
+
+jest.mock("next-intl", () => ({
+ useTranslations: () => useTranslationsMock(),
+}));
+
+/*
+ nested async server components (< ...Fetcher />) are currently breaking the render.
+ stated workarounds are not working. to get testing minimally working, overriding
+ Suspense to force display of fallback UI.
+
+ for more see https://github.com/testing-library/react-testing-library/issues/1209
+*/
+jest.mock("react", () => ({
+ ...jest.requireActual("react"),
+ Suspense: ({ fallback }: { fallback: React.Component }) => fallback,
+}));
+
+describe("SearchResults", () => {
+ it("Renders without errors", () => {
+ render(
+ ,
+ );
+
+ const component = screen.getByText("resultsHeader.message");
+ expect(component).toBeInTheDocument();
+ });
+});