Skip to content

Commit

Permalink
Merge pull request #3246 from HHS/OPS-310/3100_CAN_funding_received_form
Browse files Browse the repository at this point in the history
feat: adds CAN Funding Received form
  • Loading branch information
fpigeonjr authored Jan 13, 2025
2 parents ef82ef6 + bf1d797 commit f94edac
Show file tree
Hide file tree
Showing 16 changed files with 655 additions and 238 deletions.
50 changes: 47 additions & 3 deletions frontend/cypress/e2e/canDetail.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ describe("CAN detail page", () => {
cy.get("[data-cy='can-budget-fy-card']").should("contain", "0");
cy.get("#budget-amount").type(can504.budgetAmount);
cy.get("#budget-amount").clear();
cy.get(".usa-error-message").should("exist").contains("This is required information");
cy.get("#budget-amount").type(can504.budgetAmount);
cy.get(".usa-error-message").should("not.exist");
cy.get("#add-fy-budget").click();
Expand All @@ -197,28 +196,73 @@ describe("CAN detail page", () => {
.and("contain", `FY ${currentFiscalYear}`)
.and("contain", "$5,000,000.00");
});
it("handle funding received form", () => {
cy.visit(`/cans/${can504.number}/funding`);
cy.get("#fiscal-year-select").select(currentFiscalYear);
cy.get("#edit").click();
// check that all buttons (saved, all funding received) are disabled
cy.get("[data-cy=add-funding-received-btn]").should("be.disabled");
cy.get("[data-cy=save-btn]").should("be.disabled");
// enter amount into input
cy.get("#funding-received-amount").type("1_000_000");
cy.get("#funding-received-amount").blur();
cy.get("[data-cy=add-funding-received-btn]").should("be.enabled");
// clear and check validation
cy.get("#funding-received-amount").clear();
cy.get("[data-cy=add-funding-received-btn]").should("be.disabled");
// Test received amount over budget amount
cy.get("#funding-received-amount").type("6_000_000");
cy.get("[data-cy=add-funding-received-btn]").should("be.disabled");
cy.get(".usa-error-message").should("exist").contains("Amount cannot exceed FY Budget");
cy.get("#funding-received-amount").clear();
cy.get("#funding-received-amount").type("1_000_000");
cy.get("#funding-received-amount").blur();
cy.get("[data-cy=add-funding-received-btn]").should("be.enabled");
// enter and click on add funding received
cy.get("#notes").type("Test notes");
cy.get("[data-cy=add-funding-received-btn]").click();
// check card on the right
cy.get("[data-cy=budget-received-card]").should("exist").and("contain", "1,000,000.00");
// click on button at bottom of form
cy.get("[data-cy=save-btn]").click();
// check success alert
cy.get(".usa-alert__body").should("contain", `The CAN ${can504.nickname} has been successfully updated.`);
// check that table and card are updated
cy.get("[data-cy=budget-received-card]")
.should("exist")
.and("contain", "Received $1,000,000.00 of $5,000,000.00");
cy.get("tbody").children().should("contain", "2025").and("contain", "$1,000,000.00").and("contain", "20%");
});
it("handles cancelling from budget form", () => {
cy.visit(`/cans/${can504.number}/funding`);
cy.get("#fiscal-year-select").select(currentFiscalYear);
cy.get("#edit").click();
cy.get("#carry-forward-card").should("contain", "0");
cy.get("#carry-forward-card").should("contain", "$ 5,000,000.00");
cy.get("[data-cy='can-budget-fy-card']").should("contain", "5,000,000.00");
cy.get("#budget-amount").type("6_000_000");
cy.get("#add-fy-budget").click();
cy.get("[data-cy='can-budget-fy-card']").should("contain", "6,000,000.00");
cy.get("#save-changes").should("be.enabled");
// test funding received form
cy.get("#funding-received-amount").type("1_000_000");
cy.get("[data-cy=add-funding-received-btn]").click();
// cancel changes
cy.get("[data-cy=cancel-button]").should("be.enabled");
cy.get("[data-cy=cancel-button]").click();
cy.get(".usa-modal__heading").should(
"contain",
"Are you sure you want to cancel editing? Your changes will not be saved."
);
cy.get("[data-cy='confirm-action']").click();
cy.get("[data-cy=budget-received-card]").should("exist").and("contain", "Received $0.00 of $5,000,000.00");
cy.get("[data-cy=budget-received-card]")
.should("exist")
.and("contain", "Received $1,000,000.00 of $5,000,000.00");
cy.get("[data-cy=can-budget-fy-card]")
.should("exist")
.and("contain", "CAN Budget by FY")
.and("contain", `FY ${currentFiscalYear}`)
.and("contain", "$5,000,000.00");
// check table has one row
cy.get("tbody").children().should("have.length", 1);
});
});
10 changes: 10 additions & 0 deletions frontend/src/api/opsAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,15 @@ export const opsApi = createApi({
}),
invalidatesTags: ["Cans", "CanFunding"]
}),
addCanFundingReceived: builder.mutation({
query: ({ data }) => ({
url: `/cans-funding-received/`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: data
}),
invalidatesTags: ["Cans", "CanFunding"]
}),
getCanFundingSummary: builder.query({
query: ({ ids, fiscalYear, activePeriod, transfer, portfolio, fyBudgets }) => {
const queryParams = [];
Expand Down Expand Up @@ -433,6 +442,7 @@ export const {
useUpdateCanMutation,
useAddCanFundingBudgetsMutation,
useUpdateCanFundingBudgetMutation,
useAddCanFundingReceivedMutation,
useGetCanFundingSummaryQuery,
useGetNotificationsByUserIdQuery,
useGetNotificationsByUserIdAndAgreementIdQuery,
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import icons from "../../../uswds/img/sprite.svg";

/**
* @typedef {Object} CANBudgetFormProps
* @property {string} totalFunding
* @property {string} budgetAmount
* @property {(arg: string) => string} cn
* @property {Object} res
Expand All @@ -17,14 +18,13 @@ import icons from "../../../uswds/img/sprite.svg";
* @param {CANBudgetFormProps} props
* @returns {JSX.Element} - The component JSX.
*/
const CANBudgetForm = ({ budgetAmount, cn, res, fiscalYear, handleAddBudget, runValidate, setBudgetAmount }) => {
const CANBudgetForm = ({ totalFunding, budgetAmount, cn, res, fiscalYear, handleAddBudget, runValidate, setBudgetAmount }) => {
const fillColor = budgetAmount ? "#005ea2" : "#757575";

return (
<form
onSubmit={(e) => {
handleAddBudget(e);
setBudgetAmount("");
}}
>
<div style={{ width: "383px" }}>
Expand All @@ -38,6 +38,7 @@ const CANBudgetForm = ({ budgetAmount, cn, res, fiscalYear, handleAddBudget, run
value={budgetAmount || ""}
messages={res.getErrors("budget-amount")}
className={cn("budget-amount")}
placeholder={`$${totalFunding}`}
/>
</div>
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ describe("CANBudgetForm", () => {
await user.click(screen.getByRole("button", { name: /add fy budget/i }));

expect(defaultProps.handleAddBudget).toHaveBeenCalled();
expect(defaultProps.setBudgetAmount).toHaveBeenCalledWith("");
});

test("calls runValidate when currency input changes", () => {
Expand Down
11 changes: 0 additions & 11 deletions frontend/src/components/CANs/CANBudgetForm/suite.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import CurrencyInput from "../../UI/Form/CurrencyInput";
import TextArea from "../../UI/Form/TextArea";
import icons from "../../../uswds/img/sprite.svg";

/**
* @typedef {Object} CANFundingReceivedFormProps
* @property {(arg: string) => string} cn
* @property {Object} res
* @property {string} receivedFundingAmount
* @property {(e: React.FormEvent<HTMLFormElement>) => void} handleSubmit
* @property {(name: string, value: string) => void} runValidate
* @property { React.Dispatch<React.SetStateAction<string>>} setReceivedFundingAmount
* @property {string} notes
* @property { React.Dispatch<React.SetStateAction<string>>} setNotes
*/

/**
* @component - The CAN Funding Received Form component.
* @param {CANFundingReceivedFormProps} props
* @returns {JSX.Element} - The component JSX.
*/

const CANFundingReceivedForm = ({
cn,
res,
runValidate,
handleSubmit,
receivedFundingAmount,
setReceivedFundingAmount,
notes,
setNotes
}) => {
const isFormInValid = !receivedFundingAmount || res.hasErrors("funding-received-amount");
const fillColor = !isFormInValid ? "#005ea2" : "#757575";

return (
<form
onSubmit={(e) => {
handleSubmit(e);
}}
>
<div style={{ width: "383px" }}>
<CurrencyInput
name="funding-received-amount"
label="Funding Received"
onChange={(name, value) => {
runValidate("funding-received-amount", value);
}}
setEnteredAmount={setReceivedFundingAmount}
value={receivedFundingAmount || ""}
messages={res.getErrors("funding-received-amount")}
className={`${cn("funding-received-amount")} margin-top-0`}
/>
<TextArea
maxLength={75}
name="notes"
label="Notes (optional)"
value={notes}
onChange={(name, value) => setNotes(value)}
textAreaStyle={{ height: "51px" }}
/>{" "}
</div>
<button
className="usa-button usa-button--outline margin-top-4"
disabled={isFormInValid}
data-cy="add-funding-received-btn"
>
<svg
className="height-2 width-2 margin-right-05 cursor-pointer"
style={{ fill: fillColor }}
>
<use xlinkHref={`${icons}#add`}></use>
</svg>
Add Funding Received
</button>
</form>
);
};

export default CANFundingReceivedForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import CANFundingReceivedForm from "./CanFundingReceivedForm";

describe("CANFundingReceivedForm", () => {
let user;
const defaultProps = {
cn: vi.fn(),
res: {
hasErrors: vi.fn().mockReturnValue(false),
getErrors: vi.fn().mockReturnValue([])
},
receivedFundingAmount: "",
handleSubmit: vi.fn(),
runValidate: vi.fn(),
setReceivedFundingAmount: vi.fn(),
notes: "",
setNotes: vi.fn()
};

beforeEach(() => {
user = userEvent.setup();
vi.clearAllMocks();
});

it("renders the form correctly", () => {
render(<CANFundingReceivedForm {...defaultProps} />);

expect(screen.getByLabelText(/Funding Received/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Notes \(optional\)/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Add Funding Received/i })).toBeInTheDocument();
});

it("calls setNotes when typing in notes field", async () => {
render(<CANFundingReceivedForm {...defaultProps} />);
const textarea = screen.getByLabelText(/Notes \(optional\)/i);

await user.type(textarea, "Test note");
expect(defaultProps.setNotes).toHaveBeenCalledTimes(9);
});

it("calls setReceivedFundingAmount when typing amount", async () => {
render(<CANFundingReceivedForm {...defaultProps} />);
const input = screen.getByLabelText(/Funding Received/i);

await user.type(input, "1000");

expect(defaultProps.setReceivedFundingAmount).toHaveBeenCalledWith(1000);
});

it("calls handleSubmit when form is submitted", async () => {
render(
<CANFundingReceivedForm
{...defaultProps}
receivedFundingAmount="1000"
/>
);

await user.click(screen.getByRole("button", { name: /Add Funding Received/i }));

expect(defaultProps.handleSubmit).toHaveBeenCalled();
});

it("disables submit button when form is invalid", () => {
render(
<CANFundingReceivedForm
{...defaultProps}
res={{
...defaultProps.res,
hasErrors: vi.fn().mockReturnValue(true)
}}
/>
);

expect(screen.getByRole("button", { name: /Add Funding Received/i })).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./CanFundingReceivedForm";
Loading

0 comments on commit f94edac

Please sign in to comment.