From 5afb09ed924662bd5de1c8f2ddca5c337541a54d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:54:12 +0000 Subject: [PATCH 01/14] Bump i18next from 24.2.1 to 24.2.2 Bumps [i18next](https://github.com/i18next/i18next) from 24.2.1 to 24.2.2. - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v24.2.1...v24.2.2) --- updated-dependencies: - dependency-name: i18next dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 499bfb9e..783e1fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@mui/material-nextjs": "^6.3.1", "axios": "^1.7.9", "dotenv": "^16.4.7", - "i18next": "^24.2.1", + "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.2", "i18next-chained-backend": "^4.6.2", "i18next-http-backend": "^3.0.2", @@ -6015,9 +6015,9 @@ } }, "node_modules/i18next": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.1.tgz", - "integrity": "sha512-Q2wC1TjWcSikn1VAJg13UGIjc+okpFxQTxjVAymOnSA3RpttBQNMPf2ovcgoFVsV4QNxTfNZMAxorXZXsk4fBA==", + "version": "24.2.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz", + "integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==", "funding": [ { "type": "individual", diff --git a/package.json b/package.json index 65f2ba79..f9ab55d4 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@mui/material-nextjs": "^6.3.1", "axios": "^1.7.9", "dotenv": "^16.4.7", - "i18next": "^24.2.1", + "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.2", "i18next-chained-backend": "^4.6.2", "i18next-http-backend": "^3.0.2", From 5cfffbc13fda4cdb2bc656c1c18dcefe30b7340f Mon Sep 17 00:00:00 2001 From: "K. Allagbe" Date: Fri, 7 Feb 2025 10:26:57 -0700 Subject: [PATCH 02/14] issue #231: tests --- .../__tests__/ConfirmationPage.test.tsx | 49 +++++++++++++++++-- src/app/label-data-confirmation/page.tsx | 6 --- .../__tests__/IngredientsForm.test.tsx | 23 ++++++++- .../__tests__/modelTransformation.test.ts | 12 +++++ 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx b/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx index 20e1b737..887fa154 100644 --- a/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx +++ b/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx @@ -1,15 +1,15 @@ import FileUploaded from "@/classe/File"; +import { QuantityChips } from "@/components/QuantityChip"; import useUploadedFilesStore from "@/stores/fileStore"; import useLabelDataStore from "@/stores/labelDataStore"; +import { Quantity } from "@/types/types"; import { VERIFIED_LABEL_DATA, VERIFIED_LABEL_DATA_WITH_ID, } from "@/utils/client/constants"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import axios from "axios"; import LabelDataConfirmationPage from "../page"; -import { QuantityChips } from "@/components/QuantityChip"; -import { Quantity } from "@/types/types"; const mockedRouterPush = jest.fn(); jest.mock("next/navigation", () => ({ @@ -235,7 +235,9 @@ describe("Notes Section Tests", () => { it("should update the comment value when text is entered", () => { useLabelDataStore.getState().setLabelData(VERIFIED_LABEL_DATA); - expect(useLabelDataStore.getState().labelData?.comment).toBe("Compliant with regulations."); + expect(useLabelDataStore.getState().labelData?.comment).toBe( + "Compliant with regulations.", + ); render(); const notesTextbox = screen .getByTestId("notes-textbox") @@ -263,3 +265,42 @@ describe("Notes Section Tests", () => { expect(notesTextbox).not.toBeDisabled(); }); }); + +describe("LabelDataConfirmationPage - Ingredients Section", () => { + it("should render the nutrients table when recordKeeping is false or undefined", async () => { + useLabelDataStore.getState().setLabelData({ + ...VERIFIED_LABEL_DATA, + ingredients: { + ...VERIFIED_LABEL_DATA.ingredients, + recordKeeping: { value: false, verified: true }, + }, + }); + expect( + useLabelDataStore.getState().labelData?.ingredients.recordKeeping, + ).toEqual({ value: false, verified: true }); + + render(); + + expect(screen.getByTestId("ingredients-section")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText("ingredients.nutrients")).toBeInTheDocument(); + expect(screen.getByText("Ammonium phosphate")).toBeInTheDocument(); + }); + + useLabelDataStore.getState().setLabelData({ + ...VERIFIED_LABEL_DATA, + ingredients: { + ...VERIFIED_LABEL_DATA.ingredients, + recordKeeping: { value: true, verified: true }, + }, + }); + + expect(screen.getByTestId("ingredients-section")).toBeInTheDocument(); + await waitFor(() => { + expect( + screen.queryByText("ingredients.nutrients"), + ).not.toBeInTheDocument(); + expect(screen.queryByText("Ammonium phosphate")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/app/label-data-confirmation/page.tsx b/src/app/label-data-confirmation/page.tsx index 084e65d7..4c0b1131 100644 --- a/src/app/label-data-confirmation/page.tsx +++ b/src/app/label-data-confirmation/page.tsx @@ -37,12 +37,8 @@ import { useTranslation } from "react-i18next"; const LabelDataConfirmationPage = () => { const labelData = useLabelDataStore((state) => state.labelData); const setLabelData = useLabelDataStore((state) => state.setLabelData); - const resetLabelData = useLabelDataStore((state) => state.resetLabelData); const setComment = useLabelDataStore((state) => state.setComment); const uploadedFiles = useUploadedFilesStore((state) => state.uploadedFiles); - const clearUploadedFiles = useUploadedFilesStore( - (state) => state.clearUploadedFiles, - ); const imageFiles = uploadedFiles.map((file) => file.getFile()); const router = useRouter(); const [confirmLoading, setConfirmLoading] = useState(false); @@ -70,8 +66,6 @@ const LabelDataConfirmationPage = () => { ) .then(() => { showAlert("Label data saved successfully.", "success"); - resetLabelData(); - clearUploadedFiles(); router.push("/"); }) .catch((error) => { diff --git a/src/components/__tests__/IngredientsForm.test.tsx b/src/components/__tests__/IngredientsForm.test.tsx index 19f9a836..92b5cd1c 100644 --- a/src/components/__tests__/IngredientsForm.test.tsx +++ b/src/components/__tests__/IngredientsForm.test.tsx @@ -1,5 +1,5 @@ import { DEFAULT_LABEL_DATA, LabelData } from "@/types/types"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import IngredientsForm from "../IngredientsForm"; @@ -40,4 +40,25 @@ describe("IngredientsForm Rendering", () => { screen.getByTestId("table-container-ingredients.nutrients"), ).toBeInTheDocument(); }); + + it("should not render the nutrient section when record-keeping is set to yes, then render it again when set to no", () => { + render(); + fireEvent.click( + screen.getByTestId("radio-yes-field-ingredients.recordKeeping.value"), + ); + setTimeout(() => { + expect( + screen.queryByTestId("table-container-ingredients.nutrients"), + ).not.toBeInTheDocument(); + + fireEvent.click( + screen.getByTestId("radio-no-field-ingredients.recordKeeping.value"), + ); + setTimeout(() => { + expect( + screen.getByTestId("table-container-ingredients.nutrients"), + ).toBeInTheDocument(); + }, 350); + }, 350); + }); }); diff --git a/src/utils/server/__tests__/modelTransformation.test.ts b/src/utils/server/__tests__/modelTransformation.test.ts index d012b879..137fb11e 100644 --- a/src/utils/server/__tests__/modelTransformation.test.ts +++ b/src/utils/server/__tests__/modelTransformation.test.ts @@ -909,4 +909,16 @@ describe("mapLabelDataToInspectionUpdate", () => { }); expect(result.ingredients).toEqual({ en: [], fr: [] }); }); + + it("should return empty ingredients when recordKeeping is true", () => { + const modifiedLabelData: LabelData = { + ...labelData, + ingredients: { + ...labelData.ingredients, + recordKeeping: { value: true, verified: false }, + }, + }; + const result = mapLabelDataToInspectionUpdate(modifiedLabelData); + expect(result.ingredients).toEqual({ en: [], fr: [] }); + }); }); From 42c8bf6b2acbe6913665a07702cd6cfce21d4b36 Mon Sep 17 00:00:00 2001 From: "K. Allagbe" Date: Mon, 10 Feb 2025 10:35:45 -0700 Subject: [PATCH 03/14] issue #234: ui changes --- src/components/BaseInformationForm.tsx | 15 +- src/components/QuantityInput.tsx | 10 +- src/components/RegistrationInput.tsx | 88 ++++++ src/components/StyledDeleteButton.tsx | 36 +++ src/components/StyledListContainer.tsx | 44 +++ src/components/VerifiedFieldComponents.tsx | 17 +- src/components/VerifiedListRow.tsx | 42 +++ src/components/VerifiedQuantityList.tsx | 103 ++++++++ src/components/VerifiedQuantityMultiInput.tsx | 250 ------------------ src/components/VerifiedRegistrationList.tsx | 90 +++++++ .../__tests__/QuantityInput.test.tsx | 2 +- .../VerifiedQuantityMultiInput.test.tsx | 4 +- .../__tests__/VerifiedRadio.test.tsx | 20 +- src/utils/client/constants.ts | 10 +- 14 files changed, 452 insertions(+), 279 deletions(-) create mode 100644 src/components/RegistrationInput.tsx create mode 100644 src/components/StyledDeleteButton.tsx create mode 100644 src/components/StyledListContainer.tsx create mode 100644 src/components/VerifiedListRow.tsx create mode 100644 src/components/VerifiedQuantityList.tsx delete mode 100644 src/components/VerifiedQuantityMultiInput.tsx create mode 100644 src/components/VerifiedRegistrationList.tsx diff --git a/src/components/BaseInformationForm.tsx b/src/components/BaseInformationForm.tsx index cd453a40..f5092388 100644 --- a/src/components/BaseInformationForm.tsx +++ b/src/components/BaseInformationForm.tsx @@ -5,7 +5,8 @@ import { useEffect } from "react"; import { FormProvider, useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { VerifiedInput } from "./VerifiedFieldComponents"; -import VerifiedQuantityMultiInput from "./VerifiedQuantityMultiInput"; +import VerifiedQuantityList from "./VerifiedQuantityList"; +import VerifiedRegistrationList from "./VerifiedRegistrationList"; const BaseInformationForm: React.FC = ({ loading = false, @@ -46,13 +47,11 @@ const BaseInformationForm: React.FC = ({ path="baseInformation.name" loading={loading} /> - = ({ path="baseInformation.npk" loading={loading} /> - - - void; - onblur?: () => void; + onBlur?: () => void; verified?: boolean; } @@ -22,7 +22,7 @@ const QuantityInput = ({ disabled = false, unitRules, onFocus, - onblur, + onBlur, verified, }: QuantityInputProps) => { const { t } = useTranslation("labelDataValidator"); @@ -50,7 +50,7 @@ const QuantityInput = ({ disabled={disabled} onFocus={onFocus} onBlur={(e) => { - onblur?.(); + onBlur?.(); field.onChange(e.target.value.trim()); }} aria-label={t("quantityInput.accessibility.value")} @@ -81,13 +81,13 @@ const QuantityInput = ({ renderInput={(params) => ( { - onblur?.(); + onBlur?.(); field.onChange(e.target.value.trim()); }} error={!!error} diff --git a/src/components/RegistrationInput.tsx b/src/components/RegistrationInput.tsx new file mode 100644 index 00000000..1e3afb4a --- /dev/null +++ b/src/components/RegistrationInput.tsx @@ -0,0 +1,88 @@ +import { RegistrationType } from "@/types/types"; +import { Box, MenuItem, Select } from "@mui/material"; +import { Control, Controller, RegisterOptions } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import StyledTextField from "./StyledTextField"; + +interface RegistrationInputProps { + name: string; + control: Control; + disabled?: boolean; + typeRules?: RegisterOptions; + onFocus?: () => void; + onBlur?: () => void; + verified?: boolean; +} + +const RegistrationInput = ({ + name, + control, + disabled = false, + typeRules, + onFocus, + onBlur, + verified, +}: RegistrationInputProps) => { + const { t } = useTranslation("labelDataValidator"); + + return ( + + {/* Identifier Input Field */} + ( + { + onBlur?.(); + field.onChange(e.target.value.trim()); + }} + aria-label={t("baseInformation.fields.reg.accessibility")} + data-testid={`${name}-number-input`} + error={!!error} + helperText={error?.message ? t(error.message) : ""} + /> + )} + /> + + {/* Registration Type Selection Field */} + ( + + )} + /> + + ); +}; + +export default RegistrationInput; diff --git a/src/components/StyledDeleteButton.tsx b/src/components/StyledDeleteButton.tsx new file mode 100644 index 00000000..1fd1804b --- /dev/null +++ b/src/components/StyledDeleteButton.tsx @@ -0,0 +1,36 @@ +import DeleteIcon from "@mui/icons-material/Delete"; +import { IconButton, IconButtonProps, Tooltip } from "@mui/material"; +import React from "react"; + +interface StyledDeleteButtonProps extends IconButtonProps { + tooltip: string; + tooltipDelay?: number; + hideButton?: boolean; +} + +const StyledDeleteButton = React.forwardRef< + HTMLButtonElement, + StyledDeleteButtonProps +>( + ( + { + tooltip: tooltipTitle, + tooltipDelay: enterDelay = 1000, + hideButton, + ...props + }, + ref, + ) => { + if (hideButton) return null; + + return ( + + + + + + ); + }, +); + +export default StyledDeleteButton; diff --git a/src/components/StyledListContainer.tsx b/src/components/StyledListContainer.tsx new file mode 100644 index 00000000..9d6d6dd9 --- /dev/null +++ b/src/components/StyledListContainer.tsx @@ -0,0 +1,44 @@ +import AddIcon from "@mui/icons-material/Add"; +import { Box, BoxProps, Button } from "@mui/material"; +import React from "react"; +import { useTranslation } from "react-i18next"; + +interface StyledListContainerProps extends BoxProps { + path: string; + verified?: boolean; + onAppend: () => void; + children: React.ReactNode; +} + +const StyledListContainer: React.FC = ({ + path, + verified, + onAppend, + children, + ...boxProps +}) => { + const t = useTranslation("labelDataValidator").t; + return ( + + {children} + + {/* Add Row Button */} + + + ); +}; + +export default StyledListContainer; diff --git a/src/components/VerifiedFieldComponents.tsx b/src/components/VerifiedFieldComponents.tsx index db9a121b..f9e8e28c 100644 --- a/src/components/VerifiedFieldComponents.tsx +++ b/src/components/VerifiedFieldComponents.tsx @@ -29,6 +29,7 @@ interface VerifiedFieldWrapperProps { valuePath: string; verified: boolean; }) => ReactNode; + validate?: (callback: (valid: boolean) => void) => Promise; } export const VerifiedFieldWrapper: React.FC = ({ @@ -37,6 +38,7 @@ export const VerifiedFieldWrapper: React.FC = ({ className = "", loading = false, renderField, + validate, }) => { const { t } = useTranslation("labelDataValidator"); const { control } = useFormContext(); @@ -63,13 +65,16 @@ export const VerifiedFieldWrapper: React.FC = ({ data-testid={`verified-field-${path}`} > {renderField({ setIsFocused, control, valuePath, verified })} + + {/* Vertical Divider */} + + {/* Verified Toggle Button */} = ({ enterDelay={1000} > onChange(!value)} + onClick={() => + validate + ? validate((valid) => { + if (valid) onChange(!value); + }) + : onChange(!value) + } data-testid={`toggle-verified-btn-${verifiedPath}`} aria-label={ verified diff --git a/src/components/VerifiedListRow.tsx b/src/components/VerifiedListRow.tsx new file mode 100644 index 00000000..98c572d9 --- /dev/null +++ b/src/components/VerifiedListRow.tsx @@ -0,0 +1,42 @@ +import { Box, Divider } from "@mui/material"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import StyledDeleteButton from "./StyledDeleteButton"; + +interface VerifiedListRowProps { + verified?: boolean; + hideDelete?: boolean; + onDelete?: () => void; + isLastItem?: boolean; + children: React.ReactNode; +} + +const VerifiedListRow: React.FC = ({ + verified, + hideDelete, + onDelete, + isLastItem, + children, +}) => { + const { t } = useTranslation("labelDataValidator"); + return ( + + + {children} + + + + + + + ); +}; + +export default VerifiedListRow; diff --git a/src/components/VerifiedQuantityList.tsx b/src/components/VerifiedQuantityList.tsx new file mode 100644 index 00000000..40e2aa2c --- /dev/null +++ b/src/components/VerifiedQuantityList.tsx @@ -0,0 +1,103 @@ +import { DEFAULT_QUANTITY } from "@/types/types"; +import { Typography } from "@mui/material"; +import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; +import QuantityInput from "./QuantityInput"; +import StyledListContainer from "./StyledListContainer"; +import { VerifiedFieldWrapper } from "./VerifiedFieldComponents"; +import VerifiedListRow from "./VerifiedListRow"; + +interface VerifiedQuantityListProps { + label: string; + path: string; + unitOptions: string[]; + className?: string; + loading?: boolean; +} + +const VerifiedQuantityList: React.FC = ({ + label, + path, + unitOptions, + className = "", + loading = false, +}) => { + const { control, trigger } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: `${path}.quantities`, + }); + const quantitiesPath = `${path}.quantities`; + const quantities = useWatch({ + control, + name: quantitiesPath, + }); + + const validateFields = async (callback: (valid: boolean) => void) => { + const validationResults = await Promise.all( + fields.map((_, index) => + Promise.all([ + trigger(`${quantitiesPath}.${index}.unit`), + trigger(`${quantitiesPath}.${index}.value`), + ]), + ), + ); + const allValid = validationResults.every((result) => + result.every((isValid) => isValid), + ); + callback(allValid); + }; + + const validateDuplicateUnit = (value: string) => { + const isDuplicate = + quantities.filter((item: { unit: string }) => item.unit === value) + .length > 1; + return !isDuplicate || "errors.duplicateUnit"; + }; + + return ( + + {label} + + } + path={path} + className={className} + loading={loading} + validate={validateFields} + renderField={({ setIsFocused, control, verified }) => ( + append(DEFAULT_QUANTITY)} + > + {fields.map((fieldItem, index) => ( + remove(index)} + isLastItem={index === fields.length - 1} + > + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + verified={verified} + /> + + ))} + + )} + /> + ); +}; + +export default VerifiedQuantityList; diff --git a/src/components/VerifiedQuantityMultiInput.tsx b/src/components/VerifiedQuantityMultiInput.tsx deleted file mode 100644 index bd056ae8..00000000 --- a/src/components/VerifiedQuantityMultiInput.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { DEFAULT_QUANTITY } from "@/types/types"; -import AddIcon from "@mui/icons-material/Add"; -import CheckIcon from "@mui/icons-material/Check"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { - Box, - Button, - Divider, - IconButton, - SvgIcon, - Tooltip, - Typography, -} from "@mui/material"; -import { useState } from "react"; -import { - Controller, - useFieldArray, - useFormContext, - useWatch, -} from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import QuantityInput from "./QuantityInput"; -import StyledSkeleton from "./StyledSkeleton"; - -interface VerifiedQuantityMultiInputProps { - label: string; - path: string; - unitOptions: string[]; - className?: string; - loading?: boolean; -} - -const VerifiedQuantityMultiInput: React.FC = ({ - label, - path, - unitOptions, - className = "", - loading = false, -}) => { - const { t } = useTranslation("labelDataValidator"); - const { control, trigger } = useFormContext(); - const [isFocused, setIsFocused] = useState(false); - const [hover, setHover] = useState(false); - - const quantitiesPath = `${path}.quantities`; - const verifiedPath = `${path}.verified`; - - const { fields, append, remove } = useFieldArray({ - control, - name: quantitiesPath, - }); - - const quantities = useWatch({ - control, - name: quantitiesPath, - }); - - const verified: boolean = useWatch({ - control, - name: verifiedPath, - }); - - const toggleVerified = async ( - verified: boolean, - setVerified: (value: boolean) => void, - ) => { - const validationResults = await Promise.all( - fields.map((_, index) => - Promise.all([ - trigger(`${quantitiesPath}.${index}.unit`), - trigger(`${quantitiesPath}.${index}.value`), - ]), - ), - ); - - const allValid = validationResults.every((result) => - result.every((isValid) => isValid), - ); - - if (allValid) { - setVerified(!verified); - } - }; - - const validateDuplicateUnit = (value: string) => { - const isDuplicate = - quantities.filter((item: { unit: string }) => item.unit === value) - .length > 1; - return !isDuplicate || "errors.duplicateUnit"; - }; - - return ( - - {/* Label Section */} - - {label} - - - {loading ? ( - - ) : ( - - {/* Fields */} - - {fields.map((fieldItem, index) => { - const isLastItem = index === fields.length - 1; - - return ( - - - {/* Value & Unit Input */} - setIsFocused(true)} - onblur={() => setIsFocused(false)} - verified={verified} - /> - - {/* Delete Row Button */} - - remove(index)} - disabled={verified} - aria-label={t( - "verifiedQuantityMultiInput.accessibility.deleteRowButton", - )} - data-testid={`delete-button-${quantitiesPath}-${index}`} - > - - - - - - - ); - })} - - {/* Add Row Button */} - - - - {/* Vertical Divider */} - - - {/* Verified Toggle Button */} - ( - - toggleVerified(value, onChange)} - aria-label={t( - "verifiedQuantityMultiInput.accessibility.verifyToggleButton", - )} - data-testid={`toggle-verified-btn-${path}`} - onMouseEnter={() => setHover(true)} - onMouseLeave={() => setHover(false)} - > - {hover && verified ? ( - - - - ) : ( - - )} - - - )} - /> - - )} - - ); -}; - -export default VerifiedQuantityMultiInput; diff --git a/src/components/VerifiedRegistrationList.tsx b/src/components/VerifiedRegistrationList.tsx new file mode 100644 index 00000000..cb0a1297 --- /dev/null +++ b/src/components/VerifiedRegistrationList.tsx @@ -0,0 +1,90 @@ +import { Typography } from "@mui/material"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import RegistrationInput from "./RegistrationInput"; +import StyledListContainer from "./StyledListContainer"; +import { VerifiedFieldWrapper } from "./VerifiedFieldComponents"; +import VerifiedListRow from "./VerifiedListRow"; + +interface VerifiedRegistrationListProps { + label: string; + path: string; + registrationTypes: string[]; + className?: string; + loading?: boolean; +} + +const VerifiedRegistrationList: React.FC = ({ + label, + path, + registrationTypes, + className = "", + loading = false, +}) => { + const { control, trigger } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: `${path}.registrations`, + }); + const registrationsPath = `${path}.registrations`; + + const validateFields = async (callback: (valid: boolean) => void) => { + const validationResults = await Promise.all( + fields.map((_, index) => + Promise.all([ + trigger(`${registrationsPath}.${index}.number`), + trigger(`${registrationsPath}.${index}.type`), + ]), + ), + ); + const allValid = validationResults.every((result) => + result.every((isValid) => isValid), + ); + callback(allValid); + }; + + return ( + + {label} + + } + path={path} + className={className} + loading={loading} + validate={validateFields} + renderField={({ setIsFocused, control, verified }) => ( + append({ number: "", type: "" })} + > + {fields.map((fieldItem, index) => ( + remove(index)} + isLastItem={index === fields.length - 1} + > + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + verified={verified} + /> + + ))} + + )} + /> + ); +}; + +export default VerifiedRegistrationList; diff --git a/src/components/__tests__/QuantityInput.test.tsx b/src/components/__tests__/QuantityInput.test.tsx index c3b973c6..04a2c0ce 100644 --- a/src/components/__tests__/QuantityInput.test.tsx +++ b/src/components/__tests__/QuantityInput.test.tsx @@ -52,7 +52,7 @@ const Wrapper = ({ disabled={disabled} unitRules={unitRules} onFocus={onFocus} - onblur={onblur} + onBlur={onblur} /> + + + ); +}; + +describe("RegistrationInput rendering", () => { + it("renders correctly with default settings", () => { + render(); + + expect( + screen.getByTestId("testRegistrationInput-container"), + ).toBeInTheDocument(); + expect( + screen.getByTestId("testRegistrationInput-number-input"), + ).toBeInTheDocument(); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + expect(screen.getByTestId("submit-button")).toBeInTheDocument(); + }); + + it("renders with disabled fields when the disabled prop is true", () => { + render(); + + const identifierInput = screen + .getByTestId("testRegistrationInput-number-input") + .querySelector("input"); + const typeSelect = screen.getByRole("combobox"); + + expect(identifierInput).toBeDisabled(); + expect(typeSelect).toHaveAttribute("aria-disabled", "true"); + }); + + it("renders the correct registration type options", async () => { + render(); + const select = screen.getByRole("combobox"); + await userEvent.click(select); + expect( + screen.getByRole("option", { name: /fertilizer/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: /ingredient/i }), + ).toBeInTheDocument(); + }); +}); + +describe("RegistrationInput functionality", () => { + it("triggers validation for incorrect identifier format", async () => { + render(); + + const identifierInput = screen + .getByTestId("testRegistrationInput-number-input") + .querySelector("input"); + + if (identifierInput) { + await userEvent.type(identifierInput, "1234XYZ"); + } + + screen.getByTestId("submit-button").click(); + + const identifierError = await screen.findByText( + "errors.invalidRegistrationNumber", + ); + expect(identifierError).toBeInTheDocument(); + }); + + it("submits correct data when valid inputs are provided", async () => { + const mockSubmit = jest.fn(); + render( + , + ); + + const identifierInput = screen + .getByTestId("testRegistrationInput-number-input") + .querySelector("input"); + + if (identifierInput) { + await userEvent.clear(identifierInput); + await userEvent.type(identifierInput, "7654321B"); + } + + const typeSelect = screen.getByRole("combobox"); + await userEvent.click(typeSelect); + + const ingredientOption = screen.getByRole("option", { + name: /ingredient/i, + }); + await userEvent.click(ingredientOption); + await userEvent.click(screen.getByTestId("submit-button")); + + expect(mockSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + testRegistrationInput: { + identifier: "7654321B", + type: RegistrationType.INGREDIENT, + }, + }), + ); + }); +}); diff --git a/src/components/__tests__/StyledListContainer.test.tsx b/src/components/__tests__/StyledListContainer.test.tsx new file mode 100644 index 00000000..6d7470bf --- /dev/null +++ b/src/components/__tests__/StyledListContainer.test.tsx @@ -0,0 +1,50 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import StyledListContainer from "../StyledListContainer"; + +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, // Mock translation function + }), +})); + +describe("StyledListContainer", () => { + const mockOnAppend = jest.fn(); + const defaultProps = { + path: "testPath", + verified: false, + onAppend: mockOnAppend, + children:
Child Content
, + }; + + it("renders children correctly", () => { + render(); + expect(screen.getByTestId("child")).toBeInTheDocument(); + }); + + it("renders the add button when not verified", () => { + render(); + expect(screen.getByTestId("add-button-testPath")).toBeInTheDocument(); + }); + + it("hides the add button when verified is true", () => { + render(); + expect(screen.getByTestId("add-button-testPath")).toHaveClass("!hidden"); + }); + + it("calls onAppend when add button is clicked", () => { + render(); + const addButton = screen.getByTestId("add-button-testPath"); + fireEvent.click(addButton); + expect(mockOnAppend).toHaveBeenCalled(); + }); + + it("disables add button when verified is true", () => { + render(); + expect(screen.getByTestId("add-button-testPath")).toHaveClass("!hidden"); + }); + + it("applies correct data-testid to container", () => { + render(); + expect(screen.getByTestId("fields-container-testPath")).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/VerifiedListRow.test.tsx b/src/components/__tests__/VerifiedListRow.test.tsx new file mode 100644 index 00000000..2de6c0b6 --- /dev/null +++ b/src/components/__tests__/VerifiedListRow.test.tsx @@ -0,0 +1,63 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import VerifiedListRow from "../VerifiedListRow"; + +describe("VerifiedListRow", () => { + const mockOnDelete = jest.fn(); + const defaultProps = { + verified: false, + hideDelete: false, + onDelete: mockOnDelete, + isLastItem: false, + children:
Child Content
, + }; + + it("renders children correctly", () => { + render(); + expect(screen.getByTestId("child")).toBeInTheDocument(); + }); + + it("shows delete button when verified is false and hideDelete is false", () => { + render(); + expect(screen.getByTestId("styled-delete-button")).toBeInTheDocument(); + }); + + it("hides delete button when verified is true", () => { + render(); + expect( + screen.queryByTestId("styled-delete-button"), + ).not.toBeInTheDocument(); + }); + + it("hides delete button when hideDelete is true", () => { + render(); + expect( + screen.queryByTestId("styled-delete-button"), + ).not.toBeInTheDocument(); + }); + + it("calls onDelete when delete button is clicked", () => { + render(); + const deleteButton = screen.getByTestId("styled-delete-button"); + fireEvent.click(deleteButton); + expect(mockOnDelete).toHaveBeenCalled(); + }); + + it("renders divider with green background when verified is true", () => { + render(); + const divider = screen.getByRole("separator"); + expect(divider).toHaveClass("bg-green-500"); + }); + + it("hides divider when isLastItem is true and verified is true", () => { + render(); + + const divider = screen.getByRole("separator"); + expect(divider).toHaveClass("hidden"); + }); + + it("shows divider when isLastItem is false", () => { + render(); + const divider = screen.getByRole("separator"); + expect(divider).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/VerifiedRegistrationList.test.tsx b/src/components/__tests__/VerifiedRegistrationList.test.tsx new file mode 100644 index 00000000..348bde75 --- /dev/null +++ b/src/components/__tests__/VerifiedRegistrationList.test.tsx @@ -0,0 +1,215 @@ +import { RegistrationNumbers, RegistrationType } from "@/types/types"; +import { fireEvent, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { FormProvider, useForm } from "react-hook-form"; +import VerifiedRegistrationList from "../VerifiedRegistrationList"; + +const Wrapper = ({ + label = "Test Label", + path = "", + defaultValues = { + values: [{ identifier: "12345", type: RegistrationType.FERTILIZER }], + verified: false, + }, + loading = false, + onSubmit = jest.fn(), +}: { + label?: string; + path?: string; + defaultValues?: RegistrationNumbers; + loading?: boolean; + onSubmit?: (data: RegistrationNumbers) => void; +}) => { + const methods = useForm({ + defaultValues, + mode: "onSubmit", + }); + + return ( + +
{ + event.preventDefault(); + methods.handleSubmit(onSubmit)(); + }} + > + + + +
+ ); +}; + +describe("VerifiedRegistrationList rendering", () => { + it("renders correctly with default settings", () => { + render(); + + expect(screen.getByTestId("registration-list-label-")).toHaveTextContent( + "Test Label", + ); + expect(screen.getByTestId("add-button-")).toBeInTheDocument(); + expect(screen.getByTestId(/^toggle-verified-btn-/)).toBeInTheDocument(); + + const identifierInputs = screen.getAllByTestId(/values\.\d+-number-input/); + expect(identifierInputs.length).toBe(1); + expect( + (identifierInputs[0].querySelector("input") as HTMLInputElement).value, + ).toBe("12345"); + + const typeInputs = screen.getAllByRole("combobox"); + expect(typeInputs.length).toBe(1); + expect(typeInputs[0]).toHaveTextContent( + "baseInformation.fields.reg.type.fertilizer", + ); + }); + + it("renders the correct number of rows based on defaultValues", () => { + const defaultValues = { + values: [ + { identifier: "ABC123", type: RegistrationType.FERTILIZER }, + { identifier: "XYZ789", type: RegistrationType.INGREDIENT }, + ], + verified: false, + }; + render(); + + const fieldRows = screen.getAllByTestId("field-row"); + expect(fieldRows.length).toBe(2); + + const identifierInputs = screen.getAllByTestId(/values\.\d+-number-input/); + expect(identifierInputs.length).toBe(2); + expect( + (identifierInputs[0].querySelector("input") as HTMLInputElement).value, + ).toBe("ABC123"); + expect( + (identifierInputs[1].querySelector("input") as HTMLInputElement).value, + ).toBe("XYZ789"); + + const typeInputs = screen.getAllByRole("combobox"); + expect(typeInputs.length).toBe(2); + expect(typeInputs[0]).toHaveTextContent( + "baseInformation.fields.reg.type.fertilizer", + ); + expect(typeInputs[1]).toHaveTextContent( + "baseInformation.fields.reg.type.ingredient", + ); + }); + + it("handles loading state correctly", () => { + const { rerender } = render(); + + expect(screen.getByTestId("styled-skeleton")).toBeInTheDocument(); + expect(screen.queryByTestId("add-button-")).not.toBeInTheDocument(); + expect( + screen.queryByTestId(/values\.\d+-number-input/), + ).not.toBeInTheDocument(); + expect(screen.queryByRole("combobox")).not.toBeInTheDocument(); + + rerender(); + + expect(screen.queryByTestId("styled-skeleton")).not.toBeInTheDocument(); + expect(screen.getByTestId("add-button-")).toBeInTheDocument(); + expect(screen.getAllByTestId(/values\.\d+-number-input/).length).toBe(1); + expect(screen.getAllByRole("combobox").length).toBe(1); + }); +}); + +describe("VerifiedRegistrationList functionality", () => { + it("disables input fields and add row button when verified is true", async () => { + const defaultValues = { + values: [{ identifier: "ABC123", type: RegistrationType.FERTILIZER }], + verified: true, + }; + render(); + + screen.getAllByTestId(/values\.\d+-number-input/).forEach((field) => { + expect(field.querySelector("input")).toBeDisabled(); + }); + + screen.getAllByRole("combobox").forEach((dropdown) => { + expect(dropdown).toHaveAttribute("aria-disabled", "true"); + }); + + expect(screen.getByTestId("add-button-")).toBeDisabled(); + expect(screen.getByTestId("verified-field-")).toHaveClass( + "border-green-500 bg-gray-300", + ); + + userEvent.click(screen.getByTestId("toggle-verified-btn-.verified")); + + screen.getAllByTestId(/values\.\d+-number-input/).forEach((field) => { + expect(field.querySelector("input")).toBeDisabled(); + }); + + screen.getAllByRole("combobox").forEach((dropdown) => { + expect(dropdown).toHaveAttribute("aria-disabled", "true"); + }); + + expect(screen.getByTestId("add-button-")).toBeDisabled(); + }); + + it("handles adding and removing rows correctly", () => { + const defaultValues = { + values: [ + { identifier: "ABC123", type: RegistrationType.FERTILIZER }, + { identifier: "XYZ789", type: RegistrationType.INGREDIENT }, + ], + verified: false, + }; + render(); + + let fieldRows = screen.getAllByTestId("field-row"); + expect(fieldRows.length).toBe(2); + + const removeButtons = screen.getAllByTestId("styled-delete-button"); + fireEvent.click(removeButtons[0]); + + fieldRows = screen.queryAllByTestId("field-row"); + expect(fieldRows.length).toBe(1); + + fireEvent.click(screen.getByTestId("add-button-")); + + fieldRows = screen.getAllByTestId("field-row"); + expect(fieldRows.length).toBe(2); + }); + + it("calls onSubmit with correct values", async () => { + const mockOnSubmit = jest.fn(); + const defaultValues = { + values: [{ identifier: "12345", type: RegistrationType.FERTILIZER }], + verified: false, + }; + + render(); + + const identifierInputs = screen.getAllByTestId(/values\.\d+-number-input/); + expect(identifierInputs.length).toBe(1); + + const inputField = identifierInputs[0].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("12345"); + + await userEvent.clear(inputField); + await userEvent.type(inputField, "1234567A"); + + const typeInputs = screen.getAllByRole("combobox"); + expect(typeInputs.length).toBe(1); + + await userEvent.click(typeInputs[0]); + await userEvent.keyboard("{ArrowDown}{Enter}"); + + await userEvent.click(screen.getByTestId(/^toggle-verified-btn-/)); + await userEvent.click(screen.getByTestId("submit-button")); + + expect(mockOnSubmit).toHaveBeenCalled(); + expect(mockOnSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + values: [{ identifier: "1234567A", type: RegistrationType.INGREDIENT }], + verified: true, + }), + ); + }); +}); diff --git a/src/utils/server/__tests__/modelTransformation.test.ts b/src/utils/server/__tests__/modelTransformation.test.ts index 51c07e35..7bcfa6f6 100644 --- a/src/utils/server/__tests__/modelTransformation.test.ts +++ b/src/utils/server/__tests__/modelTransformation.test.ts @@ -350,12 +350,14 @@ describe("mapLabelDataOutputToLabelData", () => { // Base Information expect(result.baseInformation.name.value).toBe(""); expect(result.baseInformation.registrationNumbers).toEqual({ - values: [], + values: [{ identifier: "", type: "fertilizer_product" }], verified: false, }); expect(result.baseInformation.lotNumber.value).toBe(""); expect(result.baseInformation.npk.value).toBe(""); - expect(result.baseInformation.weight.quantities).toEqual([]); + expect(result.baseInformation.weight.quantities).toEqual([ + { value: "", unit: "" }, + ]); expect(result.baseInformation.density.quantities).toEqual([ { value: "", unit: "" }, ]); @@ -586,11 +588,13 @@ describe("mapInspectionToLabelData", () => { expect(result.baseInformation.name.value).toBe(""); expect(result.baseInformation.registrationNumbers).toEqual({ verified: true, - values: [], + values: [{ identifier: "", type: RegistrationType.FERTILIZER }], }); expect(result.baseInformation.lotNumber.value).toBe(""); expect(result.baseInformation.npk.value).toBe(""); - expect(result.baseInformation.weight.quantities).toEqual([]); + expect(result.baseInformation.weight.quantities).toEqual([ + { value: "", unit: "" }, + ]); expect(result.baseInformation.density.quantities).toEqual([ { value: "", unit: "" }, ]); diff --git a/src/utils/server/modelTransformation.ts b/src/utils/server/modelTransformation.ts index 84ff386d..5d3880af 100644 --- a/src/utils/server/modelTransformation.ts +++ b/src/utils/server/modelTransformation.ts @@ -1,5 +1,7 @@ import { BilingualField, + DEFAULT_QUANTITY, + DEFAULT_REGISTRATION_NUMBER, LabelData, Quantity, RegistrationType, @@ -98,16 +100,21 @@ export function mapLabelDataOutputToLabelData( name: { value: data.fertiliser_name ?? "", verified: false }, registrationNumbers: { verified: false, - values: (data.registration_number ?? []).map((reg) => ({ - identifier: reg.identifier ?? "", - type: (reg.type as RegistrationType) ?? RegistrationType.FERTILIZER, - })), + values: data.registration_number?.length + ? data.registration_number.map((reg) => ({ + identifier: reg.identifier ?? "", + type: + (reg.type as RegistrationType) ?? RegistrationType.FERTILIZER, + })) + : [DEFAULT_REGISTRATION_NUMBER], }, lotNumber: { value: data.lot_number ?? "", verified: false }, npk: { value: data.npk ?? "", verified: false }, weight: { verified: false, - quantities: (data.weight ?? []).map(quantity), + quantities: data.weight?.length + ? (data.weight ?? []).map(quantity) + : [DEFAULT_QUANTITY], }, density: { verified: false, quantities: [quantity(data.density)] }, volume: { verified: false, quantities: [quantity(data.volume)] }, @@ -161,18 +168,22 @@ export function mapInspectionToLabelData( name: { value: inspection.product.name ?? "", verified: v }, registrationNumbers: { verified: v, - values: (inspection.product.registration_numbers ?? []).map((reg) => ({ - identifier: reg.registration_number ?? "", - type: reg.is_an_ingredient - ? RegistrationType.INGREDIENT - : RegistrationType.FERTILIZER, - })), + values: inspection.product.registration_numbers?.length + ? inspection.product.registration_numbers.map((reg) => ({ + identifier: reg.registration_number ?? "", + type: reg.is_an_ingredient + ? RegistrationType.INGREDIENT + : RegistrationType.FERTILIZER, + })) + : [DEFAULT_REGISTRATION_NUMBER], }, lotNumber: { value: inspection.product.lot_number ?? "", verified: v }, npk: { value: inspection.product.npk ?? "", verified: v }, weight: { verified: v, - quantities: (inspection.product.metrics?.weight ?? []).map(quantity), + quantities: inspection.product.metrics?.weight?.length + ? inspection.product.metrics.weight.map(quantity) + : [DEFAULT_QUANTITY], }, density: { verified: v, From 9b0881fc8d93ba1218759564979c6133e114cbf8 Mon Sep 17 00:00:00 2001 From: "K. Allagbe" Date: Mon, 10 Feb 2025 17:11:46 -0700 Subject: [PATCH 10/14] issue #234: tests --- public/locales/en/labelDataValidator.json | 30 ++- public/locales/fr/labelDataValidator.json | 29 ++- src/components/RegistrationInput.tsx | 8 +- src/components/StyledListContainer.tsx | 2 +- src/components/VerifiedListRow.tsx | 1 + .../HorizontalNonLinearStepper.test.tsx | 6 +- .../__tests__/IngredientsForm.test.tsx | 24 +- .../__tests__/RegistrationInput.test.tsx | 164 +++++++++++++ .../__tests__/StyledListContainer.test.tsx | 50 ++++ .../__tests__/VerifiedListRow.test.tsx | 63 +++++ .../VerifiedRegistrationList.test.tsx | 215 ++++++++++++++++++ .../__tests__/modelTransformation.test.ts | 12 +- src/utils/server/modelTransformation.ts | 35 ++- 13 files changed, 584 insertions(+), 55 deletions(-) create mode 100644 src/components/__tests__/RegistrationInput.test.tsx create mode 100644 src/components/__tests__/StyledListContainer.test.tsx create mode 100644 src/components/__tests__/VerifiedListRow.test.tsx create mode 100644 src/components/__tests__/VerifiedRegistrationList.test.tsx diff --git a/public/locales/en/labelDataValidator.json b/public/locales/en/labelDataValidator.json index ece148e6..a7581a9d 100644 --- a/public/locales/en/labelDataValidator.json +++ b/public/locales/en/labelDataValidator.json @@ -7,12 +7,7 @@ "placeholder": "Enter name" }, "reg": { - "label": "Registration Numbers", - "placeholder": "Enter registration number", - "type": { - "fertilizer": "Fertilizer", - "ingredient": "Ingredient" - } + "label": "Registration Numbers" }, "lotNumber": { "label": "Lot Number", @@ -84,9 +79,27 @@ "unit": "Dropdown to select a unit of measurement" } }, - "verifiedQuantityMultiInput": { + "registrationInput": { + "placeholder": "Enter registration number", + "type": { + "fertilizer": "Fertilizer", + "ingredient": "Ingredient" + }, + "accessibility": { + "identifier": "Input field for registration number", + "unit": "Dropdown to select a type of registration number" + } + }, + "verifiedListRow": { + "accessibility": { + "deleteRowButton": "Button to delete this row" + } + }, + "listContainer": { + "addRow": "Add Row" + }, + "verifiedQuantityList": { "deleteRow": "Delete this row", - "addRow": "Add Row", "removeRow": "Remove", "verify": "Mark as Verified", "unverify": "Mark as Unverified", @@ -105,7 +118,6 @@ "lb/gal": "lb/gal" }, "accessibility": { - "deleteRowButton": "Button to delete this row", "addRowButton": "Button to add a new row", "valueInput": "Input field for numeric value", "unitDropdown": "Dropdown to select a unit of measurement", diff --git a/public/locales/fr/labelDataValidator.json b/public/locales/fr/labelDataValidator.json index b4572c9f..9937afb3 100644 --- a/public/locales/fr/labelDataValidator.json +++ b/public/locales/fr/labelDataValidator.json @@ -7,12 +7,7 @@ "placeholder": "Entrez le nom" }, "reg": { - "label": "Numéros d'enregistrement", - "placeholder": "Entrez le numéro d'enregistrement", - "type": { - "fertilizer": "Engrais", - "ingredient": "Ingrédient" - } + "label": "Numéros d'enregistrement" }, "lotNumber": { "label": "Numéro de lot", @@ -84,7 +79,26 @@ "unitDropdown": "Champ de saisie pour une unité de mesure" } }, - "verifiedQuantityMultiInput": { + "registrationInput": { + "placeholder": "Entrez le numéro d'enregistrement", + "type": { + "fertilizer": "Engrais", + "ingredient": "Ingrédient" + }, + "accessibility": { + "identifier": "Champ de saisie pour le numéro d'enregistrement", + "unit": "Menu déroulant pour sélectionner un type de numéro d'enregistrement" + } + }, + "verifiedListRow": { + "accessibility": { + "deleteRowButton": "Bouton pour supprimer cette ligne" + } + }, + "listContainer": { + "addRow": "Ajouter une ligne" + }, + "verifiedQuantityList": { "deleteRow": "Supprimer cette ligne", "addRow": "Ajouter une ligne", "removeRow": "Supprimer", @@ -105,7 +119,6 @@ "lb/gal": "lb/gal" }, "accessibility": { - "deleteRowButton": "Bouton pour supprimer cette ligne", "addRowButton": "Bouton pour ajouter une nouvelle ligne", "valueInput": "Champ de saisie pour une valeur numérique", "unitDropdown": "Menu déroulant pour sélectionner une unité de mesure", diff --git a/src/components/RegistrationInput.tsx b/src/components/RegistrationInput.tsx index 5e89b2e3..23481d4a 100644 --- a/src/components/RegistrationInput.tsx +++ b/src/components/RegistrationInput.tsx @@ -40,14 +40,14 @@ const RegistrationInput = ({ render={({ field, fieldState: { error } }) => ( { onBlur?.(); field.onChange(e.target.value.trim()); }} - aria-label={t("baseInformation.fields.reg.accessibility")} + aria-label={t("registrationInput.accessibility.identifier")} data-testid={`${name}-number-input`} error={!!error} helperText={error?.message ? t(error.message) : ""} @@ -73,10 +73,10 @@ const RegistrationInput = ({ disableUnderline > - {t("baseInformation.fields.reg.type.fertilizer")} + {t("registrationInput.type.fertilizer")} - {t("baseInformation.fields.reg.type.ingredient")} + {t("registrationInput.type.ingredient")} )} diff --git a/src/components/StyledListContainer.tsx b/src/components/StyledListContainer.tsx index 9d6d6dd9..e289db63 100644 --- a/src/components/StyledListContainer.tsx +++ b/src/components/StyledListContainer.tsx @@ -35,7 +35,7 @@ const StyledListContainer: React.FC = ({ disabled={verified} data-testid={`add-button-${path}`} > - {t("addRow")} + {t("listContainer.addRow")} ); diff --git a/src/components/VerifiedListRow.tsx b/src/components/VerifiedListRow.tsx index cf388208..c7bf24c1 100644 --- a/src/components/VerifiedListRow.tsx +++ b/src/components/VerifiedListRow.tsx @@ -28,6 +28,7 @@ const VerifiedListRow: React.FC = ({ tooltip={t("deleteVerifiedListRow")} hideButton={verified || hideDelete} onClick={onDelete} + aria-label={t("verifiedListRow.accessibility.deleteRowButtons")} /> diff --git a/src/components/__tests__/HorizontalNonLinearStepper.test.tsx b/src/components/__tests__/HorizontalNonLinearStepper.test.tsx index 7a62bb1e..23b01539 100644 --- a/src/components/__tests__/HorizontalNonLinearStepper.test.tsx +++ b/src/components/__tests__/HorizontalNonLinearStepper.test.tsx @@ -158,10 +158,8 @@ describe("HorizontalNonLinearStepper with StepperControls", () => { ]} />, ); - const submitButton = screen.getByText("stepper.submit"); - setTimeout(() => { - expect(submitButton).toBeDisabled(); - }, 350); + const submitButton = screen.getByText("stepper.submit").closest("button"); + expect(submitButton).toBeDisabled(); }); it("enables 'Submit' button when all steps are completed", () => { diff --git a/src/components/__tests__/IngredientsForm.test.tsx b/src/components/__tests__/IngredientsForm.test.tsx index 92b5cd1c..1a58a3aa 100644 --- a/src/components/__tests__/IngredientsForm.test.tsx +++ b/src/components/__tests__/IngredientsForm.test.tsx @@ -46,19 +46,17 @@ describe("IngredientsForm Rendering", () => { fireEvent.click( screen.getByTestId("radio-yes-field-ingredients.recordKeeping.value"), ); - setTimeout(() => { - expect( - screen.queryByTestId("table-container-ingredients.nutrients"), - ).not.toBeInTheDocument(); - fireEvent.click( - screen.getByTestId("radio-no-field-ingredients.recordKeeping.value"), - ); - setTimeout(() => { - expect( - screen.getByTestId("table-container-ingredients.nutrients"), - ).toBeInTheDocument(); - }, 350); - }, 350); + expect( + screen.queryByTestId("table-container-ingredients.nutrients"), + ).not.toBeInTheDocument(); + + fireEvent.click( + screen.getByTestId("radio-no-field-ingredients.recordKeeping.value"), + ); + + expect( + screen.getByTestId("table-container-ingredients.nutrients"), + ).toBeInTheDocument(); }); }); diff --git a/src/components/__tests__/RegistrationInput.test.tsx b/src/components/__tests__/RegistrationInput.test.tsx new file mode 100644 index 00000000..b41d5e58 --- /dev/null +++ b/src/components/__tests__/RegistrationInput.test.tsx @@ -0,0 +1,164 @@ +import { RegistrationType } from "@/types/types"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { + Control, + FormProvider, + RegisterOptions, + useForm, +} from "react-hook-form"; +import RegistrationInput from "../RegistrationInput"; + +interface WrapperProps { + name?: string; + disabled?: boolean; + typeRules?: RegisterOptions; + onFocus?: () => void; + onBlur?: () => void; + defaultValues?: Record; + onSubmit?: ( + data: Record, + ) => void; +} + +const Wrapper = ({ + name = "testRegistrationInput", + disabled = false, + typeRules, + onFocus = jest.fn(), + onBlur = jest.fn(), + defaultValues = { + testRegistrationInput: { identifier: "", type: "" }, + }, + onSubmit = jest.fn(), +}: WrapperProps) => { + const methods = useForm({ + defaultValues, + mode: "onSubmit", + }); + + return ( + +
{ + event.preventDefault(); + methods.handleSubmit(onSubmit)(); + }} + > + + + +
+ ); +}; + +describe("RegistrationInput rendering", () => { + it("renders correctly with default settings", () => { + render(); + + expect( + screen.getByTestId("testRegistrationInput-container"), + ).toBeInTheDocument(); + expect( + screen.getByTestId("testRegistrationInput-number-input"), + ).toBeInTheDocument(); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + expect(screen.getByTestId("submit-button")).toBeInTheDocument(); + }); + + it("renders with disabled fields when the disabled prop is true", () => { + render(); + + const identifierInput = screen + .getByTestId("testRegistrationInput-number-input") + .querySelector("input"); + const typeSelect = screen.getByRole("combobox"); + + expect(identifierInput).toBeDisabled(); + expect(typeSelect).toHaveAttribute("aria-disabled", "true"); + }); + + it("renders the correct registration type options", async () => { + render(); + const select = screen.getByRole("combobox"); + await userEvent.click(select); + expect( + screen.getByRole("option", { name: /fertilizer/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: /ingredient/i }), + ).toBeInTheDocument(); + }); +}); + +describe("RegistrationInput functionality", () => { + it("triggers validation for incorrect identifier format", async () => { + render(); + + const identifierInput = screen + .getByTestId("testRegistrationInput-number-input") + .querySelector("input"); + + if (identifierInput) { + await userEvent.type(identifierInput, "1234XYZ"); + } + + screen.getByTestId("submit-button").click(); + + const identifierError = await screen.findByText( + "errors.invalidRegistrationNumber", + ); + expect(identifierError).toBeInTheDocument(); + }); + + it("submits correct data when valid inputs are provided", async () => { + const mockSubmit = jest.fn(); + render( + , + ); + + const identifierInput = screen + .getByTestId("testRegistrationInput-number-input") + .querySelector("input"); + + if (identifierInput) { + await userEvent.clear(identifierInput); + await userEvent.type(identifierInput, "7654321B"); + } + + const typeSelect = screen.getByRole("combobox"); + await userEvent.click(typeSelect); + + const ingredientOption = screen.getByRole("option", { + name: /ingredient/i, + }); + await userEvent.click(ingredientOption); + await userEvent.click(screen.getByTestId("submit-button")); + + expect(mockSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + testRegistrationInput: { + identifier: "7654321B", + type: RegistrationType.INGREDIENT, + }, + }), + ); + }); +}); diff --git a/src/components/__tests__/StyledListContainer.test.tsx b/src/components/__tests__/StyledListContainer.test.tsx new file mode 100644 index 00000000..6d7470bf --- /dev/null +++ b/src/components/__tests__/StyledListContainer.test.tsx @@ -0,0 +1,50 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import StyledListContainer from "../StyledListContainer"; + +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, // Mock translation function + }), +})); + +describe("StyledListContainer", () => { + const mockOnAppend = jest.fn(); + const defaultProps = { + path: "testPath", + verified: false, + onAppend: mockOnAppend, + children:
Child Content
, + }; + + it("renders children correctly", () => { + render(); + expect(screen.getByTestId("child")).toBeInTheDocument(); + }); + + it("renders the add button when not verified", () => { + render(); + expect(screen.getByTestId("add-button-testPath")).toBeInTheDocument(); + }); + + it("hides the add button when verified is true", () => { + render(); + expect(screen.getByTestId("add-button-testPath")).toHaveClass("!hidden"); + }); + + it("calls onAppend when add button is clicked", () => { + render(); + const addButton = screen.getByTestId("add-button-testPath"); + fireEvent.click(addButton); + expect(mockOnAppend).toHaveBeenCalled(); + }); + + it("disables add button when verified is true", () => { + render(); + expect(screen.getByTestId("add-button-testPath")).toHaveClass("!hidden"); + }); + + it("applies correct data-testid to container", () => { + render(); + expect(screen.getByTestId("fields-container-testPath")).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/VerifiedListRow.test.tsx b/src/components/__tests__/VerifiedListRow.test.tsx new file mode 100644 index 00000000..2de6c0b6 --- /dev/null +++ b/src/components/__tests__/VerifiedListRow.test.tsx @@ -0,0 +1,63 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import VerifiedListRow from "../VerifiedListRow"; + +describe("VerifiedListRow", () => { + const mockOnDelete = jest.fn(); + const defaultProps = { + verified: false, + hideDelete: false, + onDelete: mockOnDelete, + isLastItem: false, + children:
Child Content
, + }; + + it("renders children correctly", () => { + render(); + expect(screen.getByTestId("child")).toBeInTheDocument(); + }); + + it("shows delete button when verified is false and hideDelete is false", () => { + render(); + expect(screen.getByTestId("styled-delete-button")).toBeInTheDocument(); + }); + + it("hides delete button when verified is true", () => { + render(); + expect( + screen.queryByTestId("styled-delete-button"), + ).not.toBeInTheDocument(); + }); + + it("hides delete button when hideDelete is true", () => { + render(); + expect( + screen.queryByTestId("styled-delete-button"), + ).not.toBeInTheDocument(); + }); + + it("calls onDelete when delete button is clicked", () => { + render(); + const deleteButton = screen.getByTestId("styled-delete-button"); + fireEvent.click(deleteButton); + expect(mockOnDelete).toHaveBeenCalled(); + }); + + it("renders divider with green background when verified is true", () => { + render(); + const divider = screen.getByRole("separator"); + expect(divider).toHaveClass("bg-green-500"); + }); + + it("hides divider when isLastItem is true and verified is true", () => { + render(); + + const divider = screen.getByRole("separator"); + expect(divider).toHaveClass("hidden"); + }); + + it("shows divider when isLastItem is false", () => { + render(); + const divider = screen.getByRole("separator"); + expect(divider).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/VerifiedRegistrationList.test.tsx b/src/components/__tests__/VerifiedRegistrationList.test.tsx new file mode 100644 index 00000000..fd33fa5a --- /dev/null +++ b/src/components/__tests__/VerifiedRegistrationList.test.tsx @@ -0,0 +1,215 @@ +import { RegistrationNumbers, RegistrationType } from "@/types/types"; +import { fireEvent, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { FormProvider, useForm } from "react-hook-form"; +import VerifiedRegistrationList from "../VerifiedRegistrationList"; + +const Wrapper = ({ + label = "Test Label", + path = "", + defaultValues = { + values: [{ identifier: "12345", type: RegistrationType.FERTILIZER }], + verified: false, + }, + loading = false, + onSubmit = jest.fn(), +}: { + label?: string; + path?: string; + defaultValues?: RegistrationNumbers; + loading?: boolean; + onSubmit?: (data: RegistrationNumbers) => void; +}) => { + const methods = useForm({ + defaultValues, + mode: "onSubmit", + }); + + return ( + +
{ + event.preventDefault(); + methods.handleSubmit(onSubmit)(); + }} + > + + + +
+ ); +}; + +describe("VerifiedRegistrationList rendering", () => { + it("renders correctly with default settings", () => { + render(); + + expect(screen.getByTestId("registration-list-label-")).toHaveTextContent( + "Test Label", + ); + expect(screen.getByTestId("add-button-")).toBeInTheDocument(); + expect(screen.getByTestId(/^toggle-verified-btn-/)).toBeInTheDocument(); + + const identifierInputs = screen.getAllByTestId(/values\.\d+-number-input/); + expect(identifierInputs.length).toBe(1); + expect( + (identifierInputs[0].querySelector("input") as HTMLInputElement).value, + ).toBe("12345"); + + const typeInputs = screen.getAllByRole("combobox"); + expect(typeInputs.length).toBe(1); + expect(typeInputs[0]).toHaveTextContent( + "registrationInput.type.fertilizer", + ); + }); + + it("renders the correct number of rows based on defaultValues", () => { + const defaultValues = { + values: [ + { identifier: "ABC123", type: RegistrationType.FERTILIZER }, + { identifier: "XYZ789", type: RegistrationType.INGREDIENT }, + ], + verified: false, + }; + render(); + + const fieldRows = screen.getAllByTestId("field-row"); + expect(fieldRows.length).toBe(2); + + const identifierInputs = screen.getAllByTestId(/values\.\d+-number-input/); + expect(identifierInputs.length).toBe(2); + expect( + (identifierInputs[0].querySelector("input") as HTMLInputElement).value, + ).toBe("ABC123"); + expect( + (identifierInputs[1].querySelector("input") as HTMLInputElement).value, + ).toBe("XYZ789"); + + const typeInputs = screen.getAllByRole("combobox"); + expect(typeInputs.length).toBe(2); + expect(typeInputs[0]).toHaveTextContent( + "registrationInput.type.fertilizer", + ); + expect(typeInputs[1]).toHaveTextContent( + "registrationInput.type.ingredient", + ); + }); + + it("handles loading state correctly", () => { + const { rerender } = render(); + + expect(screen.getByTestId("styled-skeleton")).toBeInTheDocument(); + expect(screen.queryByTestId("add-button-")).not.toBeInTheDocument(); + expect( + screen.queryByTestId(/values\.\d+-number-input/), + ).not.toBeInTheDocument(); + expect(screen.queryByRole("combobox")).not.toBeInTheDocument(); + + rerender(); + + expect(screen.queryByTestId("styled-skeleton")).not.toBeInTheDocument(); + expect(screen.getByTestId("add-button-")).toBeInTheDocument(); + expect(screen.getAllByTestId(/values\.\d+-number-input/).length).toBe(1); + expect(screen.getAllByRole("combobox").length).toBe(1); + }); +}); + +describe("VerifiedRegistrationList functionality", () => { + it("disables input fields and add row button when verified is true", async () => { + const defaultValues = { + values: [{ identifier: "ABC123", type: RegistrationType.FERTILIZER }], + verified: true, + }; + render(); + + screen.getAllByTestId(/values\.\d+-number-input/).forEach((field) => { + expect(field.querySelector("input")).toBeDisabled(); + }); + + screen.getAllByRole("combobox").forEach((dropdown) => { + expect(dropdown).toHaveAttribute("aria-disabled", "true"); + }); + + expect(screen.getByTestId("add-button-")).toBeDisabled(); + expect(screen.getByTestId("verified-field-")).toHaveClass( + "border-green-500 bg-gray-300", + ); + + userEvent.click(screen.getByTestId("toggle-verified-btn-.verified")); + + screen.getAllByTestId(/values\.\d+-number-input/).forEach((field) => { + expect(field.querySelector("input")).toBeDisabled(); + }); + + screen.getAllByRole("combobox").forEach((dropdown) => { + expect(dropdown).toHaveAttribute("aria-disabled", "true"); + }); + + expect(screen.getByTestId("add-button-")).toBeDisabled(); + }); + + it("handles adding and removing rows correctly", () => { + const defaultValues = { + values: [ + { identifier: "ABC123", type: RegistrationType.FERTILIZER }, + { identifier: "XYZ789", type: RegistrationType.INGREDIENT }, + ], + verified: false, + }; + render(); + + let fieldRows = screen.getAllByTestId("field-row"); + expect(fieldRows.length).toBe(2); + + const removeButtons = screen.getAllByTestId("styled-delete-button"); + fireEvent.click(removeButtons[0]); + + fieldRows = screen.queryAllByTestId("field-row"); + expect(fieldRows.length).toBe(1); + + fireEvent.click(screen.getByTestId("add-button-")); + + fieldRows = screen.getAllByTestId("field-row"); + expect(fieldRows.length).toBe(2); + }); + + it("calls onSubmit with correct values", async () => { + const mockOnSubmit = jest.fn(); + const defaultValues = { + values: [{ identifier: "12345", type: RegistrationType.FERTILIZER }], + verified: false, + }; + + render(); + + const identifierInputs = screen.getAllByTestId(/values\.\d+-number-input/); + expect(identifierInputs.length).toBe(1); + + const inputField = identifierInputs[0].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("12345"); + + await userEvent.clear(inputField); + await userEvent.type(inputField, "1234567A"); + + const typeInputs = screen.getAllByRole("combobox"); + expect(typeInputs.length).toBe(1); + + await userEvent.click(typeInputs[0]); + await userEvent.keyboard("{ArrowDown}{Enter}"); + + await userEvent.click(screen.getByTestId(/^toggle-verified-btn-/)); + await userEvent.click(screen.getByTestId("submit-button")); + + expect(mockOnSubmit).toHaveBeenCalled(); + expect(mockOnSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + values: [{ identifier: "1234567A", type: RegistrationType.INGREDIENT }], + verified: true, + }), + ); + }); +}); diff --git a/src/utils/server/__tests__/modelTransformation.test.ts b/src/utils/server/__tests__/modelTransformation.test.ts index 51c07e35..7bcfa6f6 100644 --- a/src/utils/server/__tests__/modelTransformation.test.ts +++ b/src/utils/server/__tests__/modelTransformation.test.ts @@ -350,12 +350,14 @@ describe("mapLabelDataOutputToLabelData", () => { // Base Information expect(result.baseInformation.name.value).toBe(""); expect(result.baseInformation.registrationNumbers).toEqual({ - values: [], + values: [{ identifier: "", type: "fertilizer_product" }], verified: false, }); expect(result.baseInformation.lotNumber.value).toBe(""); expect(result.baseInformation.npk.value).toBe(""); - expect(result.baseInformation.weight.quantities).toEqual([]); + expect(result.baseInformation.weight.quantities).toEqual([ + { value: "", unit: "" }, + ]); expect(result.baseInformation.density.quantities).toEqual([ { value: "", unit: "" }, ]); @@ -586,11 +588,13 @@ describe("mapInspectionToLabelData", () => { expect(result.baseInformation.name.value).toBe(""); expect(result.baseInformation.registrationNumbers).toEqual({ verified: true, - values: [], + values: [{ identifier: "", type: RegistrationType.FERTILIZER }], }); expect(result.baseInformation.lotNumber.value).toBe(""); expect(result.baseInformation.npk.value).toBe(""); - expect(result.baseInformation.weight.quantities).toEqual([]); + expect(result.baseInformation.weight.quantities).toEqual([ + { value: "", unit: "" }, + ]); expect(result.baseInformation.density.quantities).toEqual([ { value: "", unit: "" }, ]); diff --git a/src/utils/server/modelTransformation.ts b/src/utils/server/modelTransformation.ts index 84ff386d..5d3880af 100644 --- a/src/utils/server/modelTransformation.ts +++ b/src/utils/server/modelTransformation.ts @@ -1,5 +1,7 @@ import { BilingualField, + DEFAULT_QUANTITY, + DEFAULT_REGISTRATION_NUMBER, LabelData, Quantity, RegistrationType, @@ -98,16 +100,21 @@ export function mapLabelDataOutputToLabelData( name: { value: data.fertiliser_name ?? "", verified: false }, registrationNumbers: { verified: false, - values: (data.registration_number ?? []).map((reg) => ({ - identifier: reg.identifier ?? "", - type: (reg.type as RegistrationType) ?? RegistrationType.FERTILIZER, - })), + values: data.registration_number?.length + ? data.registration_number.map((reg) => ({ + identifier: reg.identifier ?? "", + type: + (reg.type as RegistrationType) ?? RegistrationType.FERTILIZER, + })) + : [DEFAULT_REGISTRATION_NUMBER], }, lotNumber: { value: data.lot_number ?? "", verified: false }, npk: { value: data.npk ?? "", verified: false }, weight: { verified: false, - quantities: (data.weight ?? []).map(quantity), + quantities: data.weight?.length + ? (data.weight ?? []).map(quantity) + : [DEFAULT_QUANTITY], }, density: { verified: false, quantities: [quantity(data.density)] }, volume: { verified: false, quantities: [quantity(data.volume)] }, @@ -161,18 +168,22 @@ export function mapInspectionToLabelData( name: { value: inspection.product.name ?? "", verified: v }, registrationNumbers: { verified: v, - values: (inspection.product.registration_numbers ?? []).map((reg) => ({ - identifier: reg.registration_number ?? "", - type: reg.is_an_ingredient - ? RegistrationType.INGREDIENT - : RegistrationType.FERTILIZER, - })), + values: inspection.product.registration_numbers?.length + ? inspection.product.registration_numbers.map((reg) => ({ + identifier: reg.registration_number ?? "", + type: reg.is_an_ingredient + ? RegistrationType.INGREDIENT + : RegistrationType.FERTILIZER, + })) + : [DEFAULT_REGISTRATION_NUMBER], }, lotNumber: { value: inspection.product.lot_number ?? "", verified: v }, npk: { value: inspection.product.npk ?? "", verified: v }, weight: { verified: v, - quantities: (inspection.product.metrics?.weight ?? []).map(quantity), + quantities: inspection.product.metrics?.weight?.length + ? inspection.product.metrics.weight.map(quantity) + : [DEFAULT_QUANTITY], }, density: { verified: v, From a0d7a01c2dacf8fee148dc07eef29d4b8ae8c645 Mon Sep 17 00:00:00 2001 From: "K. Allagbe" Date: Tue, 11 Feb 2025 16:41:17 -0700 Subject: [PATCH 11/14] issue #456: validation ui changes --- public/locales/en/labelDataValidator.json | 4 +- public/locales/fr/labelDataValidator.json | 4 +- src/components/OrganizationsForm.tsx | 51 ++++++++++++++----- .../__tests__/OrganizationsForm.test.tsx | 35 +++++++++++-- 4 files changed, 76 insertions(+), 18 deletions(-) diff --git a/public/locales/en/labelDataValidator.json b/public/locales/en/labelDataValidator.json index 601a3059..927ed14a 100644 --- a/public/locales/en/labelDataValidator.json +++ b/public/locales/en/labelDataValidator.json @@ -30,7 +30,9 @@ } }, "organizations": { - "stepTitle": "Organizations" + "stepTitle": "Organizations", + "mainContact": "Main Contact", + "addOrganization": "Add Organization" }, "cautions": { "stepTitle": "Cautions" diff --git a/public/locales/fr/labelDataValidator.json b/public/locales/fr/labelDataValidator.json index bbe39c8b..913c88b9 100644 --- a/public/locales/fr/labelDataValidator.json +++ b/public/locales/fr/labelDataValidator.json @@ -30,7 +30,9 @@ } }, "organizations": { - "stepTitle": "Organisations" + "stepTitle": "Organizations", + "mainContact": "Contact Principal", + "addOrganization": "Ajouter une organisation" }, "cautions": { "stepTitle": "Mises en garde" diff --git a/src/components/OrganizationsForm.tsx b/src/components/OrganizationsForm.tsx index 357078a6..0cf9bd41 100644 --- a/src/components/OrganizationsForm.tsx +++ b/src/components/OrganizationsForm.tsx @@ -10,8 +10,15 @@ import AddIcon from "@mui/icons-material/Add"; import DeleteIcon from "@mui/icons-material/Delete"; import DoneAllIcon from "@mui/icons-material/DoneAll"; import RemoveDoneIcon from "@mui/icons-material/RemoveDone"; -import { Box, Button, Tooltip } from "@mui/material"; -import { useCallback, useEffect } from "react"; +import { + Box, + Button, + FormControlLabel, + Radio, + Tooltip, + Typography, +} from "@mui/material"; +import { useCallback, useEffect, useState } from "react"; import { FieldPath, FormProvider, @@ -19,6 +26,7 @@ import { useForm, useWatch, } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { VerifiedInput } from "./VerifiedFieldComponents"; const fieldNames = Object.keys(DEFAULT_ORGANIZATION) as Array< @@ -30,6 +38,7 @@ const OrganizationsForm: React.FC = ({ setLabelData, loading = false, }) => { + const { t } = useTranslation("labelDataValidator"); const methods = useForm({ defaultValues: labelData, }); @@ -49,6 +58,8 @@ const OrganizationsForm: React.FC = ({ const save = useDebouncedSave(setLabelData); + const [mainContactIndex, setMainContactIndex] = useState(null); + useEffect(() => { const currentValues = methods.getValues(); if (JSON.stringify(currentValues) !== JSON.stringify(labelData)) { @@ -86,6 +97,22 @@ const OrganizationsForm: React.FC = ({ > + setMainContactIndex(index)} + name="mainContact" + data-testid={`main-contact-radio-${index}`} + /> + } + label={ + + {t("organizations.mainContact")} + + } + /> - - - + @@ -137,7 +162,7 @@ const OrganizationsForm: React.FC = ({ startIcon={} data-testid="add-org-btn" > - Add Organization + {t("organizations.addOrganization")} diff --git a/src/components/__tests__/OrganizationsForm.test.tsx b/src/components/__tests__/OrganizationsForm.test.tsx index 7cd33481..c074087e 100644 --- a/src/components/__tests__/OrganizationsForm.test.tsx +++ b/src/components/__tests__/OrganizationsForm.test.tsx @@ -5,7 +5,7 @@ import { LabelData, Organization, } from "@/types/types"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { act, useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import OrganizationsForm from "../OrganizationsForm"; @@ -77,7 +77,7 @@ describe("OrganizationsForm Rendering", () => { const addButton = screen.getByTestId("add-org-btn"); expect(addButton).toBeInTheDocument(); - expect(addButton).toHaveTextContent("Add Organization"); + expect(addButton).toHaveTextContent("organizations.addOrganization"); }); }); @@ -342,6 +342,35 @@ describe("OrganizationsForm Functionality", () => { const unverifyAllButton = screen.getByTestId("unverify-all-btn-0"); expect(unverifyAllButton).toBeDisabled(); }); + + it("should update the main contact selection when radio button is clicked", async () => { + render( + , + ); + + const firstRadio = screen + .getByTestId("main-contact-radio-0") + .querySelector("input"); + const secondRadio = screen + .getByTestId("main-contact-radio-1") + .querySelector("input"); + + expect(firstRadio).not.toBeChecked(); + expect(secondRadio).not.toBeChecked(); + + fireEvent.click(firstRadio!); + await waitFor(() => expect(firstRadio).toBeChecked()); + expect(secondRadio).not.toBeChecked(); + + fireEvent.click(secondRadio!); + await waitFor(() => expect(secondRadio).toBeChecked()); + expect(firstRadio).not.toBeChecked(); + }); }); describe("OrganizationsForm Edge Cases", () => { @@ -357,7 +386,7 @@ describe("OrganizationsForm Edge Cases", () => { const addButton = screen.getByTestId("add-org-btn"); expect(addButton).toBeInTheDocument(); - expect(addButton).toHaveTextContent("Add Organization"); + expect(addButton).toHaveTextContent("organizations.addOrganization"); const organizationFields = screen.queryAllByTestId(/organization-\d+/); expect(organizationFields).toHaveLength(0); From 8c5156813353065cc37afc4743ef24c12cc36c28 Mon Sep 17 00:00:00 2001 From: "K. Allagbe" Date: Tue, 11 Feb 2025 18:32:23 -0700 Subject: [PATCH 12/14] issue #456: LabelData model changes --- src/components/LabelDataValidator.tsx | 5 +- src/components/OrganizationsForm.tsx | 58 ++++++++++++----- .../__tests__/OrganizationsForm.test.tsx | 3 + src/types/types.ts | 2 + src/utils/client/constants.ts | 2 + src/utils/client/fieldValidation.ts | 14 ++-- .../__tests__/modelTransformation.test.ts | 31 +++++---- src/utils/server/modelTransformation.ts | 64 ++++++++----------- 8 files changed, 102 insertions(+), 77 deletions(-) diff --git a/src/components/LabelDataValidator.tsx b/src/components/LabelDataValidator.tsx index c197299d..babc9749 100644 --- a/src/components/LabelDataValidator.tsx +++ b/src/components/LabelDataValidator.tsx @@ -126,8 +126,9 @@ function LabelDataValidator({ }; useEffect(() => { - const verified = labelData.organizations.every((org) => - checkFieldRecord(org), + const verified = labelData.organizations.every( + ({ name, address, website, phoneNumber }) => + checkFieldRecord({ name, address, website, phoneNumber }), ); setOrganizationsStepStatus( verified ? StepStatus.Completed : StepStatus.Incomplete, diff --git a/src/components/OrganizationsForm.tsx b/src/components/OrganizationsForm.tsx index 0cf9bd41..b5c7903c 100644 --- a/src/components/OrganizationsForm.tsx +++ b/src/components/OrganizationsForm.tsx @@ -20,6 +20,7 @@ import { } from "@mui/material"; import { useCallback, useEffect, useState } from "react"; import { + Controller, FieldPath, FormProvider, useFieldArray, @@ -85,6 +86,24 @@ const OrganizationsForm: React.FC = ({ [setValue], ); + const getVerifiedFields = (org?: Organization) => + org && { + name: org.name, + address: org.address, + website: org.website, + phoneNumber: org.phoneNumber, + }; + + const handleMainContactChange = (index: number) => { + fields.forEach((_, i) => { + if (i !== index) { + setValue(`organizations.${i}.mainContact`, false); + } else { + setValue(`organizations.${i}.mainContact`, true); + } + }); + }; + return ( @@ -97,21 +116,27 @@ const OrganizationsForm: React.FC = ({ > - setMainContactIndex(index)} - name="mainContact" - data-testid={`main-contact-radio-${index}`} + ( + handleMainContactChange(index)} + name="mainContact" + data-testid={`main-contact-radio-${index}`} + /> + } + label={ + + {t("organizations.mainContact")} + + } /> - } - label={ - - {t("organizations.mainContact")} - - } + )} />