Skip to content

Commit

Permalink
allow error page to be interactive without refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-s-nava committed Jan 23, 2025
1 parent 7f94ecc commit 0f1da69
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 5 deletions.
25 changes: 20 additions & 5 deletions frontend/src/app/[locale]/search/error.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import QueryProvider from "src/app/[locale]/search/QueryProvider";
import { usePrevious } from "src/hooks/usePrevious";
import { ServerSideSearchParams } from "src/types/searchRequestURLTypes";
import { Breakpoints, ErrorProps } from "src/types/uiTypes";
import { convertSearchParamsToProperTypes } from "src/utils/search/convertSearchParamsToProperTypes";

import { useTranslations } from "next-intl";
import { ReadonlyURLSearchParams, useSearchParams } from "next/navigation";
import { useEffect } from "react";

import ContentDisplayToggle from "src/components/ContentDisplayToggle";
Expand Down Expand Up @@ -48,8 +50,25 @@ function createBlankParsedError(): ParsedError {
};
}

export default function SearchError({ error }: ErrorProps) {
export default function SearchError({ error, reset }: ErrorProps) {
const t = useTranslations("Search");
const searchParams = useSearchParams();
const previousSearchParams =
usePrevious<ReadonlyURLSearchParams>(searchParams);

useEffect(() => {
if (
reset &&
previousSearchParams &&
searchParams.toString() !== previousSearchParams?.toString()
) {
reset();
}
}, [searchParams, reset]);

useEffect(() => {
console.error(error);
}, [error]);

// The error message is passed as an object that's been stringified.
// Parse it here.
Expand All @@ -69,10 +88,6 @@ export default function SearchError({ error }: ErrorProps) {
const { agency, category, eligibility, fundingInstrument, query, status } =
convertedSearchParams;

useEffect(() => {
console.error(error);
}, [error]);

return (
<QueryProvider>
<div className="grid-container">
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/hooks/usePrevious.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useEffect, useRef } from "react";

export const usePrevious = <T>(value: T) => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
1 change: 1 addition & 0 deletions frontend/src/types/uiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export interface ErrorProps {
// Next's error boundary also includes a reset function as a prop for retries,
// but it was not needed as users can retry with new inputs in the normal page flow.
error: Error & { digest?: string };
reset?: () => unknown;
}
37 changes: 37 additions & 0 deletions frontend/tests/e2e/search/search-error.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, Page, test } from "@playwright/test";
import { BrowserContextOptions } from "playwright-core";

import { toggleCheckbox, toggleMobileSearchFilters } from "./searchSpecUtil";

interface PageProps {
page: Page;
browserName?: string;
contextOptions?: BrowserContextOptions;
}

test.describe("Search error page", () => {
test("should return an error page when expected", async ({
page,
}: PageProps) => {
// trigger error by providing an invalid status value
await page.goto("/search?status=not_a_status");

expect(page.locator(".usa-alert--error")).toBeTruthy();
});

test("should allow for performing a new search from error state", async ({
page,
}, { project }) => {
await page.goto("/search?status=not_a_status");

if (project.name.match(/[Mm]obile/)) {
await toggleMobileSearchFilters(page);
}

await toggleCheckbox(page, "status-closed");

await page.waitForURL(/status\=forecasted\,posted\,closed/, {
timeout: 5000,
});
});
});
27 changes: 27 additions & 0 deletions frontend/tests/hooks/usePrevious.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { renderHook } from "@testing-library/react";
import { usePrevious } from "src/hooks/usePrevious";

// note that these tests come from https://github.com/streamich/react-use/blob/master/tests/usePrevious.test.ts
const setUp = () =>
renderHook(({ state }) => usePrevious(state), { initialProps: { state: 0 } });

describe("usePrevious", () => {
it("should return undefined on initial render", () => {
const { result } = setUp();

expect(result.current).toBeUndefined();
});

it("should always return previous state after each update", () => {
const { result, rerender } = setUp();

rerender({ state: 2 });
expect(result.current).toBe(0);

rerender({ state: 4 });
expect(result.current).toBe(2);

rerender({ state: 6 });
expect(result.current).toBe(4);
});
});

0 comments on commit 0f1da69

Please sign in to comment.