From 561e68189271ca20604cbe5db8d0f04357a12104 Mon Sep 17 00:00:00 2001 From: Preston Mueller Date: Wed, 11 Sep 2024 16:00:37 -0400 Subject: [PATCH] feat: auto-update signins list on home page (#123) --- js/components/home.tsx | 10 +- .../operatorSignIn/operatorSignInModal.tsx | 16 ++- js/test/components/home.test.tsx | 42 ++++++++ .../operatorSignInModal.test.tsx | 98 ++++++++++++++++--- 4 files changed, 149 insertions(+), 17 deletions(-) diff --git a/js/components/home.tsx b/js/components/home.tsx index eb2c6e28..7fa15169 100644 --- a/js/components/home.tsx +++ b/js/components/home.tsx @@ -5,6 +5,11 @@ import { ReactElement, useMemo, useState } from "react"; export const Home = (): ReactElement => { const [modalOpen, setModalOpen] = useState(false); + + // Keep track of sign-in count. When this changes, we re-render + // the List component (and therefore re-download the list of sign-ins) + const [signInCount, setSignInCount] = useState(0); + // check once to avoid it changing while looking at the page // no attempt yet to verify time zones / rolling the date at a time other than midnight const today = useMemo(() => DateTime.now().toISODate(), []); @@ -54,11 +59,14 @@ export const Home = (): ReactElement => {

Today's sign ins

- +
{ + setSignInCount((count) => count + 1); + }} close={() => { setModalOpen(false); }} diff --git a/js/components/operatorSignIn/operatorSignInModal.tsx b/js/components/operatorSignIn/operatorSignInModal.tsx index 7cd2fea1..b74f491a 100644 --- a/js/components/operatorSignIn/operatorSignInModal.tsx +++ b/js/components/operatorSignIn/operatorSignInModal.tsx @@ -27,6 +27,7 @@ const submit = ( badgeEntry: BadgeEntry, setComplete: React.Dispatch>, setLoading: React.Dispatch>, + onComplete: () => void, ) => { setLoading(true); @@ -41,6 +42,7 @@ const submit = ( console.error(response.status, response.statusText); setComplete(CompleteState.SIGN_IN_ERROR); } else { + onComplete(); setComplete(CompleteState.SUCCESS); } }) @@ -56,9 +58,11 @@ const submit = ( export const OperatorSignInModal = ({ show, + onComplete, close, }: { show: boolean; + onComplete: () => void; close: () => void; }): ReactElement => { const employees = useEmployees(); @@ -71,16 +75,22 @@ export const OperatorSignInModal = ({ close(); }} > - + ); }; const OperatorSignInModalContent = ({ employees, + onComplete, close, }: { employees: ApiResult; + onComplete: () => void; close: () => void; }): ReactElement => { const [badge, setBadge] = useState(null); @@ -113,7 +123,7 @@ const OperatorSignInModalContent = ({ name={name} loading={loading} onTryAgain={() => { - submit(badge, setComplete, setLoading); + submit(badge, setComplete, setLoading, onComplete); }} /> : complete === CompleteState.BADGE_SERIAL_LOOKUP_ERROR ? @@ -138,7 +148,7 @@ const OperatorSignInModalContent = ({ badge={badge.number} loading={loading} onComplete={() => { - submit(badge, setComplete, setLoading); + submit(badge, setComplete, setLoading, onComplete); }} employees={employees} /> diff --git a/js/test/components/home.test.tsx b/js/test/components/home.test.tsx index 58acaaad..58518c4d 100644 --- a/js/test/components/home.test.tsx +++ b/js/test/components/home.test.tsx @@ -1,8 +1,34 @@ +import { fetch } from "../../browser"; import { Home } from "../../components/home"; +import { neverPromise } from "../helpers/promiseWithResolvers"; import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; +jest.mock("../../components/operatorSignIn/operatorSignInModal", () => ({ + __esModule: true, + OperatorSignInModal: ({ + show, + onComplete, + }: { + show: boolean; + onComplete: () => void; + }) => { + return show ? +
+

Fit for Duty Check

+ +
+ : null; + }, +})); + +// doMock doesn't hoist, which is needed because we're +// relying on neverPromise +jest.doMock("../../browser", () => ({ + fetch: jest.fn().mockReturnValue(neverPromise), +})); + describe("home", () => { test("can open modal", async () => { const view = render( @@ -16,4 +42,20 @@ describe("home", () => { ); expect(view.getByText(/fit for duty/i)).toBeInTheDocument(); }); + + test("can reload the List on new sign-in", async () => { + const view = render( + + + , + ); + await userEvent.click( + view.getByRole("button", { name: "Sign In Operator" }), + ); + + // Clear fetch's counter so we don't catch calls from initial render + jest.mocked(fetch).mockClear(); + await userEvent.click(view.getByRole("button", { name: "Complete" })); + expect(fetch).toHaveBeenCalledWith("/api/signin?line=blue"); + }); }); diff --git a/js/test/components/operatorSignIn/operatorSignInModal.test.tsx b/js/test/components/operatorSignIn/operatorSignInModal.test.tsx index a1529260..1a03ca87 100644 --- a/js/test/components/operatorSignIn/operatorSignInModal.test.tsx +++ b/js/test/components/operatorSignIn/operatorSignInModal.test.tsx @@ -37,7 +37,13 @@ jest.mock("../../../hooks/useNfc", () => ({ describe("OperatorSignInModal", () => { test("shows badge tap message by default", () => { - const view = render(); + const view = render( + , + ); expect(view.getByText(/waiting for badge tap/i)).toBeInTheDocument(); }); @@ -45,18 +51,28 @@ describe("OperatorSignInModal", () => { test("shows badge tap unsupported message when NFC isn't available", () => { jest.mocked(nfcSupported).mockReturnValueOnce(false); - const view = render(); + const view = render( + , + ); expect(view.getByText(/badge tap is not supported/i)).toBeInTheDocument(); }); test("can close the modal", async () => { const close = jest.fn(); - const view = render(); + const view = render( + , + ); await userEvent.click(view.getByRole("button", { name: "[x]" })); expect(close).toHaveBeenCalled(); - view.rerender(); + view.rerender( + , + ); expect(view.queryByText(/fit for duty check/i)).not.toBeInTheDocument(); }); @@ -67,7 +83,13 @@ describe("OperatorSignInModal", () => { ok: true, } as Response); - const view = render(); + const view = render( + , + ); await userEvent.type(view.getByRole("textbox"), "123"); await userEvent.click(view.getByRole("button", { name: "OK" })); @@ -80,6 +102,32 @@ describe("OperatorSignInModal", () => { expect(view.getByText("signed in successfully")).toBeInTheDocument(); }); + test("runs onComplete on successful sign-in", async () => { + putMetaData("csrf-token", "TEST-CSRF-TOKEN"); + jest.mocked(fetch).mockResolvedValueOnce({ + ok: true, + } as Response); + + const onCompleteFn = jest.fn(); + + const view = render( + , + ); + await userEvent.type(view.getByRole("textbox"), "123"); + await userEvent.click(view.getByRole("button", { name: "OK" })); + + expect(view.getByText("Step 2 of 2")).toBeInTheDocument(); + await userEvent.type(view.getByRole("textbox"), "123"); + await userEvent.click( + view.getByRole("button", { name: "Complete Fit for Duty Check" }), + ); + expect(onCompleteFn).toHaveBeenCalledOnce(); + }); + test("shows failure component on error", async () => { putMetaData("csrf-token", "TEST-CSRF-TOKEN"); jest.spyOn(console, "error").mockImplementation(() => {}); @@ -89,7 +137,13 @@ describe("OperatorSignInModal", () => { statusText: "Internal Server Error", }); - const view = render(); + const view = render( + , + ); await userEvent.type(view.getByRole("textbox"), "123"); await userEvent.click(view.getByRole("button", { name: "OK" })); @@ -104,7 +158,13 @@ describe("OperatorSignInModal", () => { test("shows failure component when no operator is found after an NFC tap", async () => { const close = jest.fn(); - const view = render(); + const view = render( + , + ); jest.mocked(useNfc).mockReturnValueOnce({ result: { status: "success", data: "bad_serial" }, @@ -113,7 +173,9 @@ describe("OperatorSignInModal", () => { const { promise, reject } = PromiseWithResolvers(); jest.mocked(fetchEmployeeByBadgeSerial).mockReturnValueOnce(promise); - view.rerender(); + view.rerender( + , + ); act(() => { reject(new Error("not found")); @@ -128,14 +190,18 @@ describe("OperatorSignInModal", () => { test("shows failure component when NFC tap fails", () => { const close = jest.fn(); - const view = render(); + const view = render( + , + ); jest.mocked(useNfc).mockReturnValueOnce({ result: { status: "error", error: "error" }, abortController: new AbortController(), }); - view.rerender(); + view.rerender( + , + ); expect( view.getByText(/something went wrong when looking for a badge tap/i), @@ -150,7 +216,9 @@ describe("OperatorSignInModal", () => { const close = jest.fn(); - const view = render(); + const view = render( + , + ); await userEvent.type(view.getByRole("textbox"), "123"); @@ -163,9 +231,13 @@ describe("OperatorSignInModal", () => { expect(view.getByText(/signed in successfully/i)).toBeInTheDocument(); - view.rerender(); + view.rerender( + , + ); - view.rerender(); + view.rerender( + , + ); expect(view.getByText(/waiting for badge tap/i)).toBeInTheDocument(); });