Skip to content

Commit

Permalink
feat: auto-update signins list on home page (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
mathcolo authored Sep 11, 2024
1 parent cb617ad commit 561e681
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 17 deletions.
10 changes: 9 additions & 1 deletion js/components/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(() => DateTime.now().toISODate(), []);
Expand Down Expand Up @@ -54,11 +59,14 @@ export const Home = (): ReactElement => {
</section>
<section className="max-w-lg mx-auto px-2 py-5">
<p className="font-semibold mb-3">Today&apos;s sign ins</p>
<List line="blue" />
<List key={signInCount} line="blue" />
</section>

<OperatorSignInModal
show={modalOpen}
onComplete={() => {
setSignInCount((count) => count + 1);
}}
close={() => {
setModalOpen(false);
}}
Expand Down
16 changes: 13 additions & 3 deletions js/components/operatorSignIn/operatorSignInModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const submit = (
badgeEntry: BadgeEntry,
setComplete: React.Dispatch<React.SetStateAction<CompleteState | null>>,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
onComplete: () => void,
) => {
setLoading(true);

Expand All @@ -41,6 +42,7 @@ const submit = (
console.error(response.status, response.statusText);
setComplete(CompleteState.SIGN_IN_ERROR);
} else {
onComplete();
setComplete(CompleteState.SUCCESS);
}
})
Expand All @@ -56,9 +58,11 @@ const submit = (

export const OperatorSignInModal = ({
show,
onComplete,
close,
}: {
show: boolean;
onComplete: () => void;
close: () => void;
}): ReactElement => {
const employees = useEmployees();
Expand All @@ -71,16 +75,22 @@ export const OperatorSignInModal = ({
close();
}}
>
<OperatorSignInModalContent employees={employees} close={close} />
<OperatorSignInModalContent
employees={employees}
onComplete={onComplete}
close={close}
/>
</Modal>
);
};

const OperatorSignInModalContent = ({
employees,
onComplete,
close,
}: {
employees: ApiResult<EmployeeList>;
onComplete: () => void;
close: () => void;
}): ReactElement => {
const [badge, setBadge] = useState<BadgeEntry | null>(null);
Expand Down Expand Up @@ -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 ?
Expand All @@ -138,7 +148,7 @@ const OperatorSignInModalContent = ({
badge={badge.number}
loading={loading}
onComplete={() => {
submit(badge, setComplete, setLoading);
submit(badge, setComplete, setLoading, onComplete);
}}
employees={employees}
/>
Expand Down
42 changes: 42 additions & 0 deletions js/test/components/home.test.tsx
Original file line number Diff line number Diff line change
@@ -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 ?
<div>
<h1>Fit for Duty Check</h1>
<button onClick={onComplete}>Complete</button>
</div>
: 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(
Expand All @@ -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(
<MemoryRouter>
<Home />
</MemoryRouter>,
);
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");
});
});
98 changes: 85 additions & 13 deletions js/test/components/operatorSignIn/operatorSignInModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,42 @@ jest.mock("../../../hooks/useNfc", () => ({

describe("OperatorSignInModal", () => {
test("shows badge tap message by default", () => {
const view = render(<OperatorSignInModal show={true} close={jest.fn()} />);
const view = render(
<OperatorSignInModal
show={true}
onComplete={jest.fn()}
close={jest.fn()}
/>,
);

expect(view.getByText(/waiting for badge tap/i)).toBeInTheDocument();
});

test("shows badge tap unsupported message when NFC isn't available", () => {
jest.mocked(nfcSupported).mockReturnValueOnce(false);

const view = render(<OperatorSignInModal show={true} close={jest.fn()} />);
const view = render(
<OperatorSignInModal
show={true}
onComplete={jest.fn()}
close={jest.fn()}
/>,
);

expect(view.getByText(/badge tap is not supported/i)).toBeInTheDocument();
});

test("can close the modal", async () => {
const close = jest.fn();
const view = render(<OperatorSignInModal show={true} close={close} />);
const view = render(
<OperatorSignInModal show={true} onComplete={jest.fn()} close={close} />,
);

await userEvent.click(view.getByRole("button", { name: "[x]" }));
expect(close).toHaveBeenCalled();
view.rerender(<OperatorSignInModal show={false} close={close} />);
view.rerender(
<OperatorSignInModal show={false} onComplete={jest.fn()} close={close} />,
);

expect(view.queryByText(/fit for duty check/i)).not.toBeInTheDocument();
});
Expand All @@ -67,7 +83,13 @@ describe("OperatorSignInModal", () => {
ok: true,
} as Response);

const view = render(<OperatorSignInModal show={true} close={jest.fn()} />);
const view = render(
<OperatorSignInModal
show={true}
onComplete={jest.fn()}
close={jest.fn()}
/>,
);
await userEvent.type(view.getByRole("textbox"), "123");
await userEvent.click(view.getByRole("button", { name: "OK" }));

Expand All @@ -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(
<OperatorSignInModal
show={true}
onComplete={onCompleteFn}
close={jest.fn()}
/>,
);
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(() => {});
Expand All @@ -89,7 +137,13 @@ describe("OperatorSignInModal", () => {
statusText: "Internal Server Error",
});

const view = render(<OperatorSignInModal show={true} close={jest.fn()} />);
const view = render(
<OperatorSignInModal
show={true}
onComplete={jest.fn()}
close={jest.fn()}
/>,
);
await userEvent.type(view.getByRole("textbox"), "123");
await userEvent.click(view.getByRole("button", { name: "OK" }));

Expand All @@ -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(<OperatorSignInModal show={true} close={jest.fn()} />);
const view = render(
<OperatorSignInModal
show={true}
onComplete={jest.fn()}
close={jest.fn()}
/>,
);

jest.mocked(useNfc).mockReturnValueOnce({
result: { status: "success", data: "bad_serial" },
Expand All @@ -113,7 +173,9 @@ describe("OperatorSignInModal", () => {
const { promise, reject } = PromiseWithResolvers<string>();
jest.mocked(fetchEmployeeByBadgeSerial).mockReturnValueOnce(promise);

view.rerender(<OperatorSignInModal show={true} close={close} />);
view.rerender(
<OperatorSignInModal show={true} onComplete={jest.fn()} close={close} />,
);

act(() => {
reject(new Error("not found"));
Expand All @@ -128,14 +190,18 @@ describe("OperatorSignInModal", () => {

test("shows failure component when NFC tap fails", () => {
const close = jest.fn();
const view = render(<OperatorSignInModal show={true} close={close} />);
const view = render(
<OperatorSignInModal show={true} onComplete={jest.fn()} close={close} />,
);

jest.mocked(useNfc).mockReturnValueOnce({
result: { status: "error", error: "error" },
abortController: new AbortController(),
});

view.rerender(<OperatorSignInModal show={true} close={close} />);
view.rerender(
<OperatorSignInModal show={true} onComplete={jest.fn()} close={close} />,
);

expect(
view.getByText(/something went wrong when looking for a badge tap/i),
Expand All @@ -150,7 +216,9 @@ describe("OperatorSignInModal", () => {

const close = jest.fn();

const view = render(<OperatorSignInModal show={true} close={close} />);
const view = render(
<OperatorSignInModal show={true} onComplete={jest.fn()} close={close} />,
);

await userEvent.type(view.getByRole("textbox"), "123");

Expand All @@ -163,9 +231,13 @@ describe("OperatorSignInModal", () => {

expect(view.getByText(/signed in successfully/i)).toBeInTheDocument();

view.rerender(<OperatorSignInModal show={false} close={close} />);
view.rerender(
<OperatorSignInModal show={false} onComplete={jest.fn()} close={close} />,
);

view.rerender(<OperatorSignInModal show={true} close={close} />);
view.rerender(
<OperatorSignInModal show={true} onComplete={jest.fn()} close={close} />,
);

expect(view.getByText(/waiting for badge tap/i)).toBeInTheDocument();
});
Expand Down

0 comments on commit 561e681

Please sign in to comment.