diff --git a/bc_obps/registration/api/v2/_operations/_operation_id/_registration/operation.py b/bc_obps/registration/api/v2/_operations/_operation_id/_registration/operation.py index 880ca8616a..99b77fdb40 100644 --- a/bc_obps/registration/api/v2/_operations/_operation_id/_registration/operation.py +++ b/bc_obps/registration/api/v2/_operations/_operation_id/_registration/operation.py @@ -31,4 +31,5 @@ def register_edit_operation_information( request: HttpRequest, operation_id: UUID, payload: OperationInformationIn ) -> Tuple[Literal[200], Operation]: + # raise Exception('d') return 200, OperationServiceV2.register_operation_information(get_current_user_guid(request), operation_id, payload) diff --git a/bc_obps/registration/api/v2/_operations/_operation_id/_registration/submission.py b/bc_obps/registration/api/v2/_operations/_operation_id/_registration/submission.py index 37202e6707..53ffdcf19c 100644 --- a/bc_obps/registration/api/v2/_operations/_operation_id/_registration/submission.py +++ b/bc_obps/registration/api/v2/_operations/_operation_id/_registration/submission.py @@ -27,6 +27,7 @@ def operation_registration_submission( request: HttpRequest, operation_id: UUID, payload: OperationRegistrationSubmissionIn ) -> Tuple[Literal[200], Operation]: # Check if all checkboxes are checked + # raise Exception('d') if not all( [payload.acknowledgement_of_review, payload.acknowledgement_of_information, payload.acknowledgement_of_records] ): diff --git a/bciers/apps/registration/app/components/operations/registration/OperationInformationForm.tsx b/bciers/apps/registration/app/components/operations/registration/OperationInformationForm.tsx index 37b7744804..925bc19444 100644 --- a/bciers/apps/registration/app/components/operations/registration/OperationInformationForm.tsx +++ b/bciers/apps/registration/app/components/operations/registration/OperationInformationForm.tsx @@ -4,7 +4,7 @@ import MultiStepBase from "@bciers/components/form/MultiStepBase"; import { OperationInformationFormData } from "apps/registration/app/components/operations/registration/types"; import { actionHandler } from "@bciers/actions"; import { RJSFSchema } from "@rjsf/utils"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { IChangeEvent } from "@rjsf/core"; import { getOperationV2 } from "@bciers/actions/api"; import { @@ -13,7 +13,6 @@ import { } from "@bciers/components/form/formDataUtils"; import { registrationOperationInformationUiSchema } from "@/registration/app/data/jsonSchema/operationInformation/registrationOperationInformation"; import { useRouter } from "next/navigation"; -import { UUID } from "crypto"; interface OperationInformationFormProps { rawFormData: OperationInformationFormData; @@ -31,18 +30,6 @@ const OperationInformationForm = ({ const router = useRouter(); const [selectedOperation, setSelectedOperation] = useState(""); const [error, setError] = useState(undefined); - const [onSubmitSuccessfulResponse, setOnSubmitSuccessfulResponse] = useState< - { id: UUID } | undefined - >(undefined); - - useEffect(() => { - if (onSubmitSuccessfulResponse) { - const nextStepUrl = `/register-an-operation/${ - onSubmitSuccessfulResponse.id - }/${step + 1}`; - router.push(nextStepUrl); - } - }, [onSubmitSuccessfulResponse]); const nestedFormData = rawFormData ? createNestedFormData(rawFormData, schema) @@ -85,8 +72,17 @@ const OperationInformationForm = ({ { body, }, - ); - // errors are handled in MultiStepBase + ).then((resolve) => { + console.log("!!!!!resolve", resolve); + if (resolve?.error) { + return { error: resolve.error }; + } else if (resolve?.id) { + // this form step needs a custom push (can't use the push in MultiStepBase) because the resolve.id is in the url + const nextStepUrl = `/register-an-operation/${resolve.id}/${step + 1}`; + router.push(nextStepUrl); + return resolve; + } + }); return response; }; const handleSelectOperationChange = async (data: any) => { @@ -108,7 +104,6 @@ const OperationInformationForm = ({ cancelUrl="/" formData={formState} onSubmit={handleSubmit} - setOnSubmitSuccessfulResponse={setOnSubmitSuccessfulResponse} schema={schema} step={step} steps={steps} diff --git a/bciers/apps/registration/app/components/operations/registration/RegistrationSubmissionForm.tsx b/bciers/apps/registration/app/components/operations/registration/RegistrationSubmissionForm.tsx index 43e38e2f44..0535bf1a27 100644 --- a/bciers/apps/registration/app/components/operations/registration/RegistrationSubmissionForm.tsx +++ b/bciers/apps/registration/app/components/operations/registration/RegistrationSubmissionForm.tsx @@ -24,8 +24,7 @@ const RegistrationSubmissionForm = ({ }: OperationRegistrationFormProps) => { const [formState, setFormState] = useState({}); const [submitButtonDisabled, setSubmitButtonDisabled] = useState(true); - const [onSubmitSuccessfulResponse, setOnSubmitSuccessfulResponse] = - useState(undefined); + const [isSubmitted, setIsSubmitted] = useState(false); const handleChange = (e: IChangeEvent) => { setFormState(e.formData); @@ -43,14 +42,22 @@ const RegistrationSubmissionForm = ({ ...e.formData, }), }, - ); - // errors are handled in MultiStepBase + ).then((resolve) => { + if (resolve?.error) { + setSubmitButtonDisabled(false); + return { error: resolve.error }; + } else { + setIsSubmitted(true); + return resolve; + } + }); + return response; }; return ( <> - {onSubmitSuccessfulResponse ? ( + {isSubmitted ? ( ) : ( { }, async () => { fetchFormEnums(); - actionHandler.mockReturnValueOnce({ id: "uuid2", name: "Operation 2" }); + actionHandler.mockResolvedValueOnce({ + id: "b974a7fc-ff63-41aa-9d57-509ebe2553a4", + }); // mock the POST response from the submit handler render( { }, ); }); - expect(mockPush).toHaveBeenCalledWith("/register-an-operation/uuid2/2"); + expect(mockPush).toHaveBeenCalledWith( + "/register-an-operation/b974a7fc-ff63-41aa-9d57-509ebe2553a4/2", + ); }, ); diff --git a/bciers/libs/components/src/form/MultiStepBase.test.tsx b/bciers/libs/components/src/form/MultiStepBase.test.tsx index f0fb7b7051..cc7165cffc 100644 --- a/bciers/libs/components/src/form/MultiStepBase.test.tsx +++ b/bciers/libs/components/src/form/MultiStepBase.test.tsx @@ -82,297 +82,297 @@ describe("The MultiStepBase component", () => { ).not.toBeInTheDocument(); }); - // it("makes the form editable when the Edit button is clicked", () => { - // useParams.mockReturnValue({ - // formSection: "1", - // operation: "create", - // } as QueryParams); - // render(); - // expect(screen.getByText(/test field1/i)).toHaveAttribute( - // "class", - // "read-only-widget whitespace-pre-line", - // ); - // const editButton = screen.getByRole("button", { name: /Edit/i }); - // fireEvent.click(editButton); - // // this confirms the form is editable because the label is accompanied by an - // expect(screen.getByLabelText(/field1/i)).toHaveValue("test field1"); - // }); - - // it("shows the header with correct steps (formSectionTitles) and no submission step", () => { - // useParams.mockReturnValue({ - // formSection: "1", - // operation: "create", - // } as QueryParams); - // render(); - // const headerSteps = screen.getAllByTestId(/multistep-header-title/i); - // expect(headerSteps).toHaveLength(3); - // expect(headerSteps[0]).toHaveTextContent(/page1/i); - // expect(headerSteps[1]).toHaveTextContent(/page2/i); - // expect(headerSteps[2]).toHaveTextContent(/page3/i); - // }); - - // it("navigation buttons work on first form page when no baseUrl is given", async () => { - // mockOnSubmit.mockReturnValue({ id: "uuid" }); - // render( - // , - // ); - // expect(screen.getByTestId("field-template-label")).toHaveTextContent( - // "page1", - // ); - - // const saveAndContinueButton = screen.getByRole("button", { - // name: /Save and Continue/i, - // }); - // expect(screen.getByLabelText(/field1*/i)).toHaveValue("test field1"); - // expect(screen.getByRole("button", { name: /Back/i })).toBeDisabled(); - // expect(screen.getByRole("button", { name: /Cancel/i })).not.toBeDisabled(); - // expect(screen.getByRole("link", { name: /Cancel/i })).toHaveAttribute( - // "href", - // "cancelurl.com", - // ); - // expect(saveAndContinueButton).not.toBeDisabled(); - // fireEvent.click(saveAndContinueButton); - // expect(mockOnSubmit).toHaveBeenCalled(); - // expect(mockPush).not.toHaveBeenCalled(); - // }); - - // it("navigation and submit buttons work on subsequent form page", async () => { - // mockOnSubmit.mockReturnValue({ id: 1 }); - // render( - // , - // ); - // expect(screen.getByTestId("field-template-label")).toHaveTextContent( - // "page2", - // ); - - // const saveAndContinueButton = screen.getByRole("button", { - // name: /Save and Continue/i, - // }); - // expect(screen.getByRole("button", { name: /Back/i })).not.toBeDisabled(); - // expect(screen.getByRole("link", { name: /Back/i })).toHaveAttribute( - // "href", - // "baseurl.com/1", - // ); - // expect(screen.getByRole("button", { name: /Cancel/i })).not.toBeDisabled(); - // expect(screen.getByRole("link", { name: /Cancel/i })).toHaveAttribute( - // "href", - // "cancelurl.com", - // ); - // expect(saveAndContinueButton).not.toBeDisabled(); - // fireEvent.click(saveAndContinueButton); - // expect(mockOnSubmit).toHaveBeenCalled(); - // await waitFor(() => { - // expect(mockPush).toHaveBeenCalledWith("baseurl.com/3"); - // }); - // }); - - // it("navigation and submit buttons work on last form page", async () => { - // render( - // , - // ); - // expect(screen.getByTestId("field-template-label")).toHaveTextContent( - // "page3", - // ); - - // const submitButton = screen.getByRole("button", { - // name: /test submit button text/i, - // }); - - // expect(screen.getByRole("button", { name: /Back/i })).not.toBeDisabled(); - // expect(screen.getByRole("link", { name: /Back/i })).toHaveAttribute( - // "href", - // "baseurl.com/2", - // ); - // expect(screen.getByRole("button", { name: /Cancel/i })).not.toBeDisabled(); - // expect(screen.getByRole("link", { name: /Cancel/i })).toHaveAttribute( - // "href", - // "cancelurl.com", - // ); - // expect( - // screen.getByRole("button", { name: /test submit button text/i }), - // ).not.toBeDisabled(); - // fireEvent.click(submitButton); - // expect(mockOnSubmit).toHaveBeenCalled(); - // expect(mockPush).not.toHaveBeenCalled(); - // }); - - // it("submission is disabled if form is still submitting", async () => { - // useParams.mockReturnValue({ - // formSection: "1", - // operation: "create", - // } as QueryParams); - // const user = userEvent.setup(); - // let resolve: (v: unknown) => void; - - // const mockPromiseOnSubmit = vitest.fn().mockReturnValue( - // new Promise((_resolve) => { - // resolve = _resolve; - // }), - // ); - - // render( - // , - // ); - // const saveAndContinueButton = screen.getByRole("button", { - // name: /Save and Continue/i, - // }); - - // await act(async () => { - // await user.click(saveAndContinueButton); - // }); - - // waitFor(() => { - // expect(saveAndContinueButton).toBeDisabled(); - // }); - - // await act(async () => { - // resolve(vitest.fn); - // }); - - // expect(saveAndContinueButton).not.toBeDisabled(); - // }); - - // it("shows an error if there was a problem with form validation", () => { - // useParams.mockReturnValue({ - // formSection: "1", - // operation: "create", - // } as QueryParams); - - // render(); - // const saveAndContinueButton = screen.getByRole("button", { - // name: /Save and Continue/i, - // }); - // fireEvent.click(saveAndContinueButton); - // expect(screen.getByRole("alert")).toBeVisible(); - // expect(mockOnSubmit).not.toHaveBeenCalled(); // submit function is not called because we hit validation errors first - // }); - - // it("shows an error if passed one", () => { - // render( - // , - // ); - // expect(screen.getByRole("alert")).toBeVisible(); - // expect(screen.getByText("Test error")).toBeVisible(); - // }); - - // it("shows an error if response returns an error", async () => { - // mockOnSubmit.mockReturnValueOnce({ error: "whoopsie" }); - // render( - // , - // ); - - // const saveAndContinueButton = screen.getByRole("button", { - // name: /Save and Continue/i, - // }); - - // fireEvent.click(saveAndContinueButton); - // await waitFor(() => { - // expect(mockOnSubmit).toHaveBeenCalled(); - // expect(screen.getByRole("alert")).toBeVisible(); - // expect(screen.getByText("whoopsie")).toBeVisible(); - // }); - // }); - - // it("clears old errors on submission", async () => { - // render( - // , - // ); - // const saveAndContinueButton = screen.getByRole("button", { - // name: /Save and Continue/i, - // }); - // act(() => { - // fireEvent.click(saveAndContinueButton); - // }); - // await waitFor(() => { - // expect(mockOnSubmit).toHaveBeenCalled(); - // expect(screen.queryByRole("alert")).not.toBeInTheDocument(); - // }); - // }); - - // it("calls the onChange prop when the form changes", () => { - // useParams.mockReturnValue({ - // formSection: "1", - // operation: "create", - // } as QueryParams); - - // const changeHandler = vi.fn(); - // render( - // , - // ); - // const input = screen.getByLabelText(/field1*/i); - // fireEvent.change(input, { target: { value: "new value" } }); - - // expect(changeHandler).toHaveBeenCalled(); - // }); - - // it("renders children", () => { - // useParams.mockReturnValue({ - // formSection: "1", - // operation: "create", - // } as QueryParams); - - // render( - // - //
Test child
- //
, - // ); - // expect(screen.getByTestId("test-child")).toBeVisible(); - // }); + it("makes the form editable when the Edit button is clicked", () => { + useParams.mockReturnValue({ + formSection: "1", + operation: "create", + } as QueryParams); + render(); + expect(screen.getByText(/test field1/i)).toHaveAttribute( + "class", + "read-only-widget whitespace-pre-line", + ); + const editButton = screen.getByRole("button", { name: /Edit/i }); + fireEvent.click(editButton); + // this confirms the form is editable because the label is accompanied by an + expect(screen.getByLabelText(/field1/i)).toHaveValue("test field1"); + }); + + it("shows the header with correct steps (formSectionTitles) and no submission step", () => { + useParams.mockReturnValue({ + formSection: "1", + operation: "create", + } as QueryParams); + render(); + const headerSteps = screen.getAllByTestId(/multistep-header-title/i); + expect(headerSteps).toHaveLength(3); + expect(headerSteps[0]).toHaveTextContent(/page1/i); + expect(headerSteps[1]).toHaveTextContent(/page2/i); + expect(headerSteps[2]).toHaveTextContent(/page3/i); + }); + + it("navigation buttons work on first form page when no baseUrl is given", async () => { + mockOnSubmit.mockReturnValue({ id: "uuid" }); + render( + , + ); + expect(screen.getByTestId("field-template-label")).toHaveTextContent( + "page1", + ); + + const saveAndContinueButton = screen.getByRole("button", { + name: /Save and Continue/i, + }); + expect(screen.getByLabelText(/field1*/i)).toHaveValue("test field1"); + expect(screen.getByRole("button", { name: /Back/i })).toBeDisabled(); + expect(screen.getByRole("button", { name: /Cancel/i })).not.toBeDisabled(); + expect(screen.getByRole("link", { name: /Cancel/i })).toHaveAttribute( + "href", + "cancelurl.com", + ); + expect(saveAndContinueButton).not.toBeDisabled(); + fireEvent.click(saveAndContinueButton); + expect(mockOnSubmit).toHaveBeenCalled(); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it("navigation and submit buttons work on subsequent form page", async () => { + mockOnSubmit.mockReturnValue({ id: 1 }); + render( + , + ); + expect(screen.getByTestId("field-template-label")).toHaveTextContent( + "page2", + ); + + const saveAndContinueButton = screen.getByRole("button", { + name: /Save and Continue/i, + }); + expect(screen.getByRole("button", { name: /Back/i })).not.toBeDisabled(); + expect(screen.getByRole("link", { name: /Back/i })).toHaveAttribute( + "href", + "baseurl.com/1", + ); + expect(screen.getByRole("button", { name: /Cancel/i })).not.toBeDisabled(); + expect(screen.getByRole("link", { name: /Cancel/i })).toHaveAttribute( + "href", + "cancelurl.com", + ); + expect(saveAndContinueButton).not.toBeDisabled(); + fireEvent.click(saveAndContinueButton); + expect(mockOnSubmit).toHaveBeenCalled(); + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith("baseurl.com/3"); + }); + }); + + it("navigation and submit buttons work on last form page", async () => { + render( + , + ); + expect(screen.getByTestId("field-template-label")).toHaveTextContent( + "page3", + ); + + const submitButton = screen.getByRole("button", { + name: /test submit button text/i, + }); + + expect(screen.getByRole("button", { name: /Back/i })).not.toBeDisabled(); + expect(screen.getByRole("link", { name: /Back/i })).toHaveAttribute( + "href", + "baseurl.com/2", + ); + expect(screen.getByRole("button", { name: /Cancel/i })).not.toBeDisabled(); + expect(screen.getByRole("link", { name: /Cancel/i })).toHaveAttribute( + "href", + "cancelurl.com", + ); + expect( + screen.getByRole("button", { name: /test submit button text/i }), + ).not.toBeDisabled(); + fireEvent.click(submitButton); + expect(mockOnSubmit).toHaveBeenCalled(); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it("submission is disabled if form is still submitting", async () => { + useParams.mockReturnValue({ + formSection: "1", + operation: "create", + } as QueryParams); + const user = userEvent.setup(); + let resolve: (v: unknown) => void; + + const mockPromiseOnSubmit = vitest.fn().mockReturnValue( + new Promise((_resolve) => { + resolve = _resolve; + }), + ); + + render( + , + ); + const saveAndContinueButton = screen.getByRole("button", { + name: /Save and Continue/i, + }); + + await act(async () => { + await user.click(saveAndContinueButton); + }); + + waitFor(() => { + expect(saveAndContinueButton).toBeDisabled(); + }); + + await act(async () => { + resolve(vitest.fn); + }); + + expect(saveAndContinueButton).not.toBeDisabled(); + }); + + it("shows an error if there was a problem with form validation", () => { + useParams.mockReturnValue({ + formSection: "1", + operation: "create", + } as QueryParams); + + render(); + const saveAndContinueButton = screen.getByRole("button", { + name: /Save and Continue/i, + }); + fireEvent.click(saveAndContinueButton); + expect(screen.getByRole("alert")).toBeVisible(); + expect(mockOnSubmit).not.toHaveBeenCalled(); // submit function is not called because we hit validation errors first + }); + + it("shows an error if passed one", () => { + render( + , + ); + expect(screen.getByRole("alert")).toBeVisible(); + expect(screen.getByText("Test error")).toBeVisible(); + }); + + it("shows an error if response returns an error", async () => { + mockOnSubmit.mockReturnValueOnce({ error: "whoopsie" }); + render( + , + ); + + const saveAndContinueButton = screen.getByRole("button", { + name: /Save and Continue/i, + }); + + fireEvent.click(saveAndContinueButton); + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalled(); + expect(screen.getByRole("alert")).toBeVisible(); + expect(screen.getByText("whoopsie")).toBeVisible(); + }); + }); + + it("clears old errors on submission", async () => { + render( + , + ); + const saveAndContinueButton = screen.getByRole("button", { + name: /Save and Continue/i, + }); + act(() => { + fireEvent.click(saveAndContinueButton); + }); + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalled(); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + }); + + it("calls the onChange prop when the form changes", () => { + useParams.mockReturnValue({ + formSection: "1", + operation: "create", + } as QueryParams); + + const changeHandler = vi.fn(); + render( + , + ); + const input = screen.getByLabelText(/field1*/i); + fireEvent.change(input, { target: { value: "new value" } }); + + expect(changeHandler).toHaveBeenCalled(); + }); + + it("renders children", () => { + useParams.mockReturnValue({ + formSection: "1", + operation: "create", + } as QueryParams); + + render( + +
Test child
+
, + ); + expect(screen.getByTestId("test-child")).toBeVisible(); + }); }); diff --git a/bciers/libs/components/src/form/MultiStepBase.tsx b/bciers/libs/components/src/form/MultiStepBase.tsx index f3b8f666dc..26287ded25 100644 --- a/bciers/libs/components/src/form/MultiStepBase.tsx +++ b/bciers/libs/components/src/form/MultiStepBase.tsx @@ -29,7 +29,6 @@ interface MultiStepBaseProps { uiSchema: UiSchema; submitButtonDisabled?: boolean; customValidate?: any; - setOnSubmitSuccessfulResponse?: (error: undefined) => void; // Use this if the parent component needs the successful `onSubmit` response to do further actions, e.g. the RegistrationSubmissionForm shows a confirmation message after a successful submission } // Modified MultiStepFormBase meant to facilitate more modularized Multi-step forms @@ -55,7 +54,6 @@ const MultiStepBase = ({ uiSchema, submitButtonDisabled, customValidate, - setOnSubmitSuccessfulResponse, }: MultiStepBaseProps) => { const [isSubmitting, setIsSubmitting] = useState(false); const [isEditMode, setIsEditMode] = useState(false); @@ -73,17 +71,17 @@ const MultiStepBase = ({ const response = await onSubmit(data); if (response?.error) { setError(response?.error); + setIsSubmitting(false); return; } - if (setOnSubmitSuccessfulResponse) setOnSubmitSuccessfulResponse(response); if (isNotFinalStep && baseUrl) { const nextStepUrl = `${baseUrl}/${step + 1}${ baseUrlParams ? `?${baseUrlParams}` : "" }`; router.push(nextStepUrl); + setIsSubmitting(false); } - setIsSubmitting(false); }; const isDisabled = (disabled && !isEditMode) || isSubmitting;