Skip to content

Commit a8b5053

Browse files
committed
test: administration tests
1 parent 98fa139 commit a8b5053

3 files changed

Lines changed: 294 additions & 35 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useRouter, useSearchParams } from "next/navigation";
5+
import useKey from "@bciers/utils/src/useKey";
6+
7+
import { UUID } from "crypto";
8+
import SingleStepTaskListForm from "@bciers/components/form/SingleStepTaskListForm";
9+
import { RJSFSchema, UiSchema } from "@rjsf/utils";
10+
import { IChangeEvent } from "@rjsf/core";
11+
import {
12+
OperationInformationFormData,
13+
OperationInformationPartialFormData,
14+
} from "./types";
15+
import { actionHandler } from "@bciers/actions";
16+
import {
17+
RegistrationPurposes,
18+
regulatedOperationPurposes,
19+
} from "apps/registration/app/components/operations/registration/enums";
20+
import {
21+
FormMode,
22+
FrontEndRoles,
23+
OperationTypes,
24+
} from "@bciers/utils/src/enums";
25+
import { useSessionRole } from "@bciers/utils/src/sessionUtils";
26+
import Note from "@bciers/components/layout/Note";
27+
import Link from "next/link";
28+
import ConfirmChangeOfFieldModal from "@/registration/app/components/operations/registration/ConfirmChangeOfFieldModal";
29+
import { useFileUploadWidget } from "@bciers/components/form/widgets/FileWidget";
30+
31+
const OperationInformationForm = ({
32+
formData,
33+
operationId,
34+
schema: initialSchema,
35+
eioSchema,
36+
generalSchema,
37+
uiSchema,
38+
}: {
39+
formData: OperationInformationPartialFormData;
40+
operationId: UUID;
41+
schema: RJSFSchema;
42+
eioSchema: RJSFSchema;
43+
generalSchema: RJSFSchema;
44+
uiSchema: UiSchema;
45+
}) => {
46+
const [error, setError] = useState(undefined);
47+
const [schema, setSchema] = useState(initialSchema);
48+
const [confirmedFormData, setConfirmedFormData] = useState(formData);
49+
const [
50+
pendingChangeRegistrationPurpose,
51+
setPendingChangeRegistrationPurpose,
52+
] = useState("");
53+
const [isConfirmPurposeChangeModalOpen, setIsConfirmPurposeChangeModalOpen] =
54+
useState<boolean>(false);
55+
const [key, resetKey] = useKey();
56+
const [formMode, setFormMode] = useState(FormMode.READ_ONLY);
57+
58+
const router = useRouter();
59+
// To get the user's role from the session
60+
const role = useSessionRole();
61+
const searchParams = useSearchParams();
62+
63+
const [fileWidgetContext, submitWithFiles] = useFileUploadWidget();
64+
65+
const isRedirectedFromContacts = searchParams.get("from_contacts") as string;
66+
67+
function checkMissingRepresentative(data: any) {
68+
if (data && data.status && data.registration_purpose) {
69+
return (
70+
data.status === "Registered" &&
71+
(!data.operation_representatives ||
72+
data.operation_representatives.length === 0)
73+
);
74+
} else return false;
75+
}
76+
const [isMissingRepresentative, setIsMissingRepresentative] = useState(
77+
checkMissingRepresentative(formData),
78+
);
79+
80+
const updateConfirmedFormData = (newPurpose: string) => {
81+
const isEio =
82+
newPurpose === RegistrationPurposes.ELECTRICITY_IMPORT_OPERATION;
83+
const newFormData = {
84+
...confirmedFormData,
85+
registration_purpose: newPurpose,
86+
// When switching to EIO, set the type to EIO since that's the only valid option
87+
...(isEio && { type: OperationTypes.EIO }),
88+
};
89+
setConfirmedFormData(newFormData);
90+
91+
if (isEio) {
92+
setSchema(eioSchema);
93+
} else {
94+
setSchema(generalSchema);
95+
}
96+
};
97+
98+
const handleSubmit = async (data: {
99+
formData?: OperationInformationFormData;
100+
}) => {
101+
setError(undefined);
102+
const pathToRevalidate = `/operations/${operationId}`;
103+
104+
const response = await submitWithFiles(
105+
data.formData,
106+
`registration/operations/${operationId}`,
107+
"POST",
108+
`/operations/${operationId}`,
109+
);
110+
111+
if (response?.error) {
112+
// Users get this error when they select a contact that's missing address information. We include a link to the Contacts page because the user has to fix the error from there, not here in the operation form.
113+
if (response.error.includes("Please return to Contacts")) {
114+
const splitError = response.error.split("Contacts");
115+
response.error = (
116+
<>
117+
{splitError[0]} <Link href={"/contacts"}>Contacts</Link>
118+
{splitError[1]}
119+
</>
120+
);
121+
}
122+
setError(response.error);
123+
return { error: response.error };
124+
}
125+
126+
if (!data.formData?.opted_in_operation) return;
127+
const response2 = await actionHandler(
128+
`registration/operations/${operationId}/registration/opted-in-operation-detail`,
129+
"PUT",
130+
pathToRevalidate,
131+
{
132+
body: JSON.stringify(data.formData?.opted_in_operation),
133+
},
134+
);
135+
136+
if (response2?.error) {
137+
setError(response2.error);
138+
return { error: response2.error };
139+
}
140+
};
141+
142+
const cancelRegistrationPurposeChange = () => {
143+
setPendingChangeRegistrationPurpose("");
144+
setFormMode(FormMode.EDIT); // Keep form in edit mode after remount
145+
resetKey();
146+
setIsConfirmPurposeChangeModalOpen(false);
147+
};
148+
149+
const confirmRegistrationPurposeChange = () => {
150+
if (pendingChangeRegistrationPurpose !== "") {
151+
updateConfirmedFormData(pendingChangeRegistrationPurpose);
152+
setFormMode(FormMode.EDIT); // Keep form in edit mode after remount
153+
resetKey();
154+
setIsConfirmPurposeChangeModalOpen(false);
155+
}
156+
setPendingChangeRegistrationPurpose("");
157+
};
158+
159+
const handleSelectedPurposeChange = (newSelectedPurpose: string) => {
160+
if (newSelectedPurpose && confirmedFormData.registration_purpose) {
161+
setIsConfirmPurposeChangeModalOpen(true);
162+
setPendingChangeRegistrationPurpose(newSelectedPurpose);
163+
}
164+
};
165+
166+
return (
167+
<>
168+
{isRedirectedFromContacts && !role.includes("cas_") && (
169+
<Note variant="important">
170+
To remove the current operation representative, please select a new
171+
contact to replace them.
172+
</Note>
173+
)}
174+
<ConfirmChangeOfFieldModal
175+
open={isConfirmPurposeChangeModalOpen}
176+
onCancel={cancelRegistrationPurposeChange}
177+
onConfirm={confirmRegistrationPurposeChange}
178+
modalText={
179+
<>
180+
<div>
181+
Are you sure you want to change your registration purpose? If you
182+
proceed,
183+
</div>
184+
<ul className="list-disc pl-5 mt-2">
185+
<li>
186+
Some operation information you have entered will be deleted.
187+
</li>
188+
<li>
189+
If this operation’s report is in progress, it will be deleted
190+
and restarted.
191+
</li>
192+
<li>Past years’ reports will be unaffected.</li>
193+
</ul>
194+
</>
195+
}
196+
confirmButtonText="Change registration purpose"
197+
/>
198+
<SingleStepTaskListForm
199+
key={key}
200+
allowEdit={!role.includes("cas_")}
201+
mode={formMode}
202+
error={error}
203+
schema={schema}
204+
uiSchema={uiSchema}
205+
formData={confirmedFormData ?? {}}
206+
onSubmit={handleSubmit}
207+
onChange={(e: IChangeEvent) => {
208+
const newSelectedPurpose = e.formData?.section3?.registration_purpose;
209+
if (newSelectedPurpose !== confirmedFormData.registration_purpose) {
210+
handleSelectedPurposeChange(newSelectedPurpose);
211+
}
212+
setIsMissingRepresentative(checkMissingRepresentative(e.formData));
213+
}}
214+
onCancel={() => router.push("/operations")}
215+
formContext={{
216+
operationId,
217+
isRegulatedOperation: regulatedOperationPurposes.includes(
218+
confirmedFormData.registration_purpose as RegistrationPurposes,
219+
),
220+
isOptedOut:
221+
formData.opted_in_operation?.final_reporting_year !== null,
222+
isCasDirector: role === FrontEndRoles.CAS_DIRECTOR,
223+
isEio: confirmedFormData.registration_purpose?.match(
224+
RegistrationPurposes.ELECTRICITY_IMPORT_OPERATION.valueOf(),
225+
),
226+
status: confirmedFormData.status,
227+
missing_representative_alert: isMissingRepresentative,
228+
...fileWidgetContext,
229+
}}
230+
/>
231+
</>
232+
);
233+
};
234+
235+
export default OperationInformationForm;

bciers/apps/administration/app/components/operations/OperationInformationPage.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ const OperationInformationPage = async ({
3636

3737
const uiSchema = await createAdministrationOperationInformationUiSchema();
3838

39-
console.log(operation);
40-
4139
return (
4240
<>
4341
<NewTabBanner />

bciers/apps/administration/tests/components/operations/OperationInformationForm.test.tsx

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,16 @@ const formData = {
146146
secondary_naics_code_id: 2,
147147
operation_has_multiple_operators: true,
148148
activities: [1, 2],
149-
boundary_map: { id: 1, name: "testboundary.pdf", status: "Clean" },
150-
process_flow_diagram: { id: 2, name: "processflow.pdf", status: "Clean" },
149+
boundary_map: JSON.stringify({
150+
id: 1,
151+
name: "testboundary.pdf",
152+
status: "Clean",
153+
}),
154+
process_flow_diagram: JSON.stringify({
155+
id: 2,
156+
name: "processflow.pdf",
157+
status: "Clean",
158+
}),
151159
multiple_operators_array: [
152160
{
153161
mo_is_extraprovincial_company: false,
@@ -262,10 +270,10 @@ describe("the OperationInformationForm component", () => {
262270
// 2 file inputs
263271
expect(screen.getByText(/Process Flow Diagram/i)).toBeVisible();
264272
expect(screen.getByText(/Boundary Map/i)).toBeVisible();
265-
expect(screen.getByText(/testpdf.pdf/i)).toBeVisible();
266-
expect(screen.getByText(/testpdf2.pdf/i)).toBeVisible();
273+
expect(screen.getByText(/testboundary.pdf/i)).toBeVisible();
274+
expect(screen.getByText(/processflow.pdf/i)).toBeVisible();
267275
expect(
268-
screen.getAllByRole("link", {
276+
screen.getAllByRole("button", {
269277
name: /preview/i,
270278
}),
271279
).toHaveLength(2);
@@ -360,7 +368,7 @@ describe("the OperationInformationForm component", () => {
360368
expect(screen.queryByRole("button", { name: "Edit" })).toBeNull();
361369
});
362370

363-
it.only("should edit and save the form", async () => {
371+
it("should edit and save the form", async () => {
364372
const uiSchema = await createAdministrationOperationInformationUiSchema();
365373
render(
366374
<OperationInformationForm
@@ -837,7 +845,6 @@ describe("the OperationInformationForm component", () => {
837845
new_entrant_application: {
838846
type: "string",
839847
title: "New Entrant Application and Statutory Declaration",
840-
format: "data-url",
841848
},
842849
},
843850
},
@@ -882,15 +889,21 @@ describe("the OperationInformationForm component", () => {
882889
expect(actionHandler).toHaveBeenCalledTimes(1);
883890
expect(actionHandler).toHaveBeenCalledWith(
884891
`registration/operations/${operationId}`,
885-
"PUT",
892+
"POST",
886893
`/operations/${operationId}`,
887894
{
888-
body: JSON.stringify({
889-
name: "Operation 5",
890-
type: "Single Facility Operation",
891-
registration_purpose: "New Entrant Operation",
892-
new_entrant_application:
893-
"data:application/pdf;name=mock_file.pdf;base64,dGVzdA==",
895+
body: expect.toSatisfy((submittedFormData) => {
896+
expect(Object.fromEntries(submittedFormData)).toEqual({
897+
payload: JSON.stringify({
898+
name: "Operation 5",
899+
type: "Single Facility Operation",
900+
registration_purpose: "New Entrant Operation",
901+
new_entrant_application:
902+
'{"name":"mock_file.pdf","status":"Unscanned"}',
903+
}),
904+
new_entrant_application: mockFile,
905+
});
906+
return true;
894907
}),
895908
},
896909
);
@@ -941,12 +954,16 @@ describe("the OperationInformationForm component", () => {
941954
registration_purpose: RegistrationPurposes.REPORTING_OPERATION,
942955
regulated_products: [1],
943956
operation_representatives: [1],
944-
boundary_map: { id: 1, name: "testboundary.pdf", status: "Clean" },
945-
process_flow_diagram: {
957+
boundary_map: JSON.stringify({
958+
id: 1,
959+
name: "testboundary.pdf",
960+
status: "Clean",
961+
}),
962+
process_flow_diagram: JSON.stringify({
946963
id: 2,
947964
name: "processflow.pdf",
948965
status: "Clean",
949-
},
966+
}),
950967
};
951968
useSessionRole.mockReturnValue(FrontEndRoles.INDUSTRY_USER_ADMIN);
952969

@@ -988,24 +1005,33 @@ describe("the OperationInformationForm component", () => {
9881005
expect(actionHandler).toHaveBeenCalledTimes(1);
9891006
expect(actionHandler).toHaveBeenCalledWith(
9901007
`registration/operations/${operationId}`,
991-
"PUT",
1008+
"POST",
9921009
`/operations/${operationId}`,
9931010
{
994-
body: JSON.stringify({
995-
name: "Operation 3",
996-
type: "Single Facility Operation",
997-
naics_code_id: 1,
998-
secondary_naics_code_id: 2,
999-
process_flow_diagram: {
1000-
id: 2,
1001-
name: "processflow.pdf",
1002-
status: "Clean",
1003-
},
1004-
boundary_map: { id: 1, name: "testboundary.pdf", status: "Clean" },
1005-
operation_has_multiple_operators: false,
1006-
registration_purpose: "Reporting Operation",
1007-
operation_representatives: [2],
1008-
activities: [1, 2],
1011+
body: expect.toSatisfy((submittedFormData) => {
1012+
expect(Object.fromEntries(submittedFormData)).toEqual({
1013+
payload: JSON.stringify({
1014+
name: "Operation 3",
1015+
type: "Single Facility Operation",
1016+
naics_code_id: 1,
1017+
secondary_naics_code_id: 2,
1018+
process_flow_diagram: JSON.stringify({
1019+
id: 2,
1020+
name: "processflow.pdf",
1021+
status: "Clean",
1022+
}),
1023+
boundary_map: JSON.stringify({
1024+
id: 1,
1025+
name: "testboundary.pdf",
1026+
status: "Clean",
1027+
}),
1028+
operation_has_multiple_operators: false,
1029+
registration_purpose: "Reporting Operation",
1030+
operation_representatives: [2],
1031+
activities: [1, 2],
1032+
}),
1033+
});
1034+
return true;
10091035
}),
10101036
},
10111037
);

0 commit comments

Comments
 (0)