From 8faa5354a8cc870c1fa6176359254ffa792cda4e Mon Sep 17 00:00:00 2001 From: Jesus Vasquez Date: Tue, 5 Mar 2024 17:42:30 -0500 Subject: [PATCH] Cleanup, comments, e2e --- components/Button/Button.stories.tsx | 31 -------- components/Button/Button.tsx | 47 ------------ components/Form/Button.tsx | 50 +++++++++++++ components/Form/ClientField.tsx | 19 ++++- components/Form/ErrorMessage.tsx | 8 ++- components/Form/FileInput.tsx | 24 ++++++- components/Form/RadioGroup.tsx | 36 ++++++++-- components/Form/SelectInput.tsx | 1 + components/Form/StatusField.tsx | 25 ++++--- components/Form/ToggleField.tsx | 33 +++++++-- components/Form/index.tsx | 102 +++++++++++++-------------- components/Tooltip/Tooltip.tsx | 75 -------------------- e2e/form.spec.ts | 68 ++++++++++++++++++ e2e/testFile.txt | 1 + 14 files changed, 288 insertions(+), 232 deletions(-) delete mode 100644 components/Button/Button.stories.tsx delete mode 100644 components/Button/Button.tsx create mode 100644 components/Form/Button.tsx delete mode 100644 components/Tooltip/Tooltip.tsx create mode 100644 e2e/form.spec.ts create mode 100644 e2e/testFile.txt diff --git a/components/Button/Button.stories.tsx b/components/Button/Button.stories.tsx deleted file mode 100644 index ad992c7..0000000 --- a/components/Button/Button.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react" -import { Button } from "./Button" - -const meta: Meta = { - title: "Button", - component: Button, - args: { - intent: "primary", - underline: false, - children: "Button", - size: "lg", - }, - argTypes: { - intent: { - options: ["primary", "secondary"], - control: { type: "select" }, - }, - size: { - options: ["sm", "lg"], - control: { type: "select" }, - }, - }, -} - -type Story = StoryObj - -export const Default: Story = { - render: (args) => + ); +}; diff --git a/components/Form/ClientField.tsx b/components/Form/ClientField.tsx index 6af2e4c..2614492 100644 --- a/components/Form/ClientField.tsx +++ b/components/Form/ClientField.tsx @@ -1,21 +1,34 @@ +/** + * ClientField Component + * + * Renders a list of testing centers with associated client selection inputs. + * Each testing center allows for the selection of a client from a provided list. + */ + import { faClock } from "@fortawesome/free-regular-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import React from "react" import SelectInput from "./SelectInput" import { Utils } from "./Utils" -// Define a type for your props to improve type-checking +/** + * Props for ClientField component. + * + * @param testingCenters Array of testing center names to be displayed. + * @param clients Array of client names available for selection. + */ + type ClientFieldProps = { testingCenters: string[] clients: string[] } -// Destructure the props in the function parameter const ClientField: React.FC = ({ testingCenters, clients }) => { return (
+ {/* Render a selection input for each testing center, allowing a client to be chosen for each. */} {testingCenters.map((testingCenter) => ( -
+
{testingCenter}
diff --git a/components/Form/ErrorMessage.tsx b/components/Form/ErrorMessage.tsx index 55a8928..acf10f6 100644 --- a/components/Form/ErrorMessage.tsx +++ b/components/Form/ErrorMessage.tsx @@ -1,8 +1,14 @@ +/** + * ErrorMessage Component + * + * Displays an error message if a form field has been touched and an error exists. + */ + import React from "react" const ErrorMessage = ({ touched, error }: { touched: boolean; error?: string }) => { if (touched && error) { - return

{error}

+ return

{error}

} return null } diff --git a/components/Form/FileInput.tsx b/components/Form/FileInput.tsx index acad4a4..796c4c4 100644 --- a/components/Form/FileInput.tsx +++ b/components/Form/FileInput.tsx @@ -1,3 +1,10 @@ +/** + * FileInput Component + * + * Renders a file input with drag-and-drop support. Displays the selected file's name and size, + * and simulates file upload progress. Integrates with Formik for form state management. + */ + import { faFile } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useField, useFormikContext } from "formik"; @@ -17,10 +24,17 @@ const FileInput: React.FC = ({ label, ...props }) => { const [field, meta] = useField(props); const { setFieldValue } = useFormikContext(); const fileInputRef = useRef(null); - const [dragging, setDragging] = useState(false); // State to handle drag styling - const [fileInfo, setFileInfo] = useState<{ name: string; size: number } | null>(null); // State to store file info + + // State to manage drag-over styling + const [dragging, setDragging] = useState(false); + + // State to store information about the selected file + const [fileInfo, setFileInfo] = useState<{ name: string; size: number } | null>(null); + + // State to simulate and display upload progress const [uploadProgress, setUploadProgress] = useState(0); + // Simulates file upload progress const simulateUpload = () => { let progress = 0; const interval = setInterval(() => { @@ -32,6 +46,7 @@ const FileInput: React.FC = ({ label, ...props }) => { }, 20); // Adjust time interval to control speed of the animation }; + // Handles file selection via input change event const handleChange = (event: React.ChangeEvent) => { const { files } = event.target; const file = files ? files[0] : null; @@ -43,12 +58,14 @@ const FileInput: React.FC = ({ label, ...props }) => { } }; + // Opens the file input dialog const handleClick = () => { if (fileInputRef.current) { fileInputRef.current.click(); } }; + // Handles file drop const handleDrop = (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); @@ -65,7 +82,7 @@ const FileInput: React.FC = ({ label, ...props }) => { } }; - +// Manages drag-over state for styling const handleDragOver = (event: React.DragEvent) => { event.preventDefault(); if (!dragging) { @@ -73,6 +90,7 @@ const FileInput: React.FC = ({ label, ...props }) => { } }; + // Resets dragging state when dragging leaves the drop area const handleDragLeave = (event: React.DragEvent) => { event.preventDefault(); setDragging(false); diff --git a/components/Form/RadioGroup.tsx b/components/Form/RadioGroup.tsx index 7a10ae0..d077c79 100644 --- a/components/Form/RadioGroup.tsx +++ b/components/Form/RadioGroup.tsx @@ -1,3 +1,10 @@ +/** + * RadioGroupField Component + * + * Renders a group of radio buttons using Radix UI with Formik integration. + * Supports custom options, labels, and an optional separator. + */ + import * as RadioGroup from "@radix-ui/react-radio-group" import { useField } from "formik" import React from "react" @@ -5,22 +12,37 @@ import ErrorMessage from "./ErrorMessage" import RequiredAsterisk from "./RequiredAsterisk" import Separator from "./Separator" -const RadioGroupField = ({ +/** + * Props for RadioGroupField. + * + * @param name The name attribute for the radio group, used for form submission. + * @param label Display label for the radio group. + * @param separator Optional boolean to render a visual separator above the radio group. + * @param options Array of option strings that the radio group will display. + * @param props Additional props passed to the radio group, including `required`. + */ + +interface RadioGroupFieldProps { + name: string; + label: string; + separator?: boolean; + options: string[]; + [key: string]: any; // This allows for additional props not explicitly defined in the interface. +} + +const RadioGroupField: React.FC = ({ name, label, separator, options, ...props -}: { - name: string - label: string - separator?: boolean - options: string[] - [x: string]: any // Allow any other props }) => { const { required, ...restProps } = props + + // State and helpers from Formik for managing radio group value const [field, meta, helpers] = useField({ ...restProps, name }) + // Handles radio option change, updating Formik's state const handleChange = (value: string) => { helpers.setValue(value) } diff --git a/components/Form/SelectInput.tsx b/components/Form/SelectInput.tsx index 678388e..2207115 100644 --- a/components/Form/SelectInput.tsx +++ b/components/Form/SelectInput.tsx @@ -4,6 +4,7 @@ import ErrorMessage from "./ErrorMessage" import RequiredAsterisk from "./RequiredAsterisk" const SelectInput = ({ label, ...props }: { label?: string; name: string; [x: string]: any }) => { + // Since `useField` only needs `name`, and optionally `type` and `value`, we destruct those from props const { required, ...restProps } = props const [field, meta] = useField(restProps) diff --git a/components/Form/StatusField.tsx b/components/Form/StatusField.tsx index ea553f7..378480a 100644 --- a/components/Form/StatusField.tsx +++ b/components/Form/StatusField.tsx @@ -1,21 +1,28 @@ +/** + * StatusField Component + * + * Displays a status label with a customizable color. This component is typically used + * to indicate the status of a process or entity (e.g., success, error) within the application. + * Includes a visual separator and supports dynamic text and color customization through props. + */ + import React from "react" import Separator from "./Separator" -const StatusField = ({ - label, - statusLabel, - statusColor, -}: { - label: string - statusLabel: string - statusColor: string -}) => { +interface StatusFieldProps { + label: string; + statusLabel: string; + statusColor: string; +} + +const StatusField: React.FC = ({ label, statusLabel, statusColor }) => { return (

{label}

+ {/* Dynamically styled based on status */} {statusLabel}
) diff --git a/components/Form/ToggleField.tsx b/components/Form/ToggleField.tsx index eb17e8e..282c411 100644 --- a/components/Form/ToggleField.tsx +++ b/components/Form/ToggleField.tsx @@ -1,3 +1,10 @@ +/** + * ToggleField Component + * + * Renders a toggle switch using Radix UI, integrated with Formik for form state management. + * Supports custom child content and displays an error message if validation fails. + */ + import * as SwitchPrimitive from "@radix-ui/react-switch" import { useField } from "formik" import React, { useState } from "react" @@ -7,6 +14,14 @@ import Separator from "./Separator" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faClock } from "@fortawesome/free-regular-svg-icons" +/** + * Props for ToggleField. + * + * @param children ReactNode - The content to be displayed alongside the toggle switch. + * @extends React.InputHTMLAttributes - Inherits standard input attributes. + */ + + const ToggleField = ({ children, ...props @@ -14,9 +29,20 @@ const ToggleField = ({ children: React.ReactNode } & React.InputHTMLAttributes) => { const { required, ...restProps } = props + + // Formik hook for managing form state and validation const [field, meta] = useField({ ...(restProps as { name: string }), type: "checkbox" }) + + // Local state to manage the checked status of the switch const [checked, setChecked] = useState(false) + // Handles checked state change, updates Formik's state accordingly + const handleCheckedChange = (isChecked: boolean) => { + setChecked(isChecked); + field.onChange({ target: { name: field.name, value: isChecked } }); + }; + + return (
@@ -30,14 +56,11 @@ const ToggleField = ({ { - setChecked(isChecked) - field.onChange({ target: { name: field.name, value: isChecked } }) - }} + onCheckedChange={(isChecked) => handleCheckedChange(isChecked)} > -
+
Toggle ON | Select Tolerance Level diff --git a/components/Form/index.tsx b/components/Form/index.tsx index 69b4722..e90bbe2 100644 --- a/components/Form/index.tsx +++ b/components/Form/index.tsx @@ -1,45 +1,43 @@ "use client" -import axios from "axios" -import { Form, Formik, useFormikContext } from "formik" -import React, { useState } from "react" +// React and Formik imports +import React, { useState } from "react"; +import { Form, Formik } from "formik"; +import axios from "axios"; -//Dependencies +// UI components and utilities +import { Button } from "./Button"; +import ClientField from "./ClientField"; +import FileInput from "./FileInput"; +import RadioGroup from "./RadioGroup"; +import SelectInput from "./SelectInput"; +import StatusField from "./StatusField"; +import ToggleField from "./ToggleField"; -//Input fields -import ClientField from "./ClientField" -import { data } from "./data" -import FileInput from "./FileInput" -import { initialValues, validationSchema } from "./formSchemas" -import RadioGroup from "./RadioGroup" -import SelectInput from "./SelectInput" -import StatusField from "./StatusField" -import ToggleField from "./ToggleField" +// Data and schema and types +import { FormValues } from "./types"; +import { data } from "./data"; +import { initialValues, validationSchema } from "./formSchemas"; -import { FormValues } from "./types" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faXmark } from "@fortawesome/free-solid-svg-icons" - -{/* Close button */} -const CloseButton: React.FC = () => { - const { resetForm,setFieldValue } = useFormikContext(); - const handleReset = () => { - resetForm(); - setFieldValue("manifestFile", ""); - } - - return ( - - ) -}; +// Icons +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +/** + * FormComponent: Main form component for document upload. + * + * This component handles the document upload form, including input fields for document details, + * file upload, and form submission. It uses Formik for form state management and validation, + * and Axios for submission to a backend endpoint. + */ const FormComponent: React.FC = () => { + // State to track if the form has been successfully submitted const [formSubmitted, setFormSubmitted] = useState(false); + // Function to handle form submission const submitForm = (values: FormValues) => { + //TODO implement form submission return axios.post( `/api/submitForm`, { @@ -56,7 +54,7 @@ const FormComponent: React.FC = () => { return ( <> - + {/* Formik setup for handling form state, validation, and submission */} { return (
- - {/* Title */} + {/* Close button - allows resetting the form to initial state */} + + {/* Title - provides context for the form's purpose */}

Document Upload

- {/* Form layout */} + {/* Form layout - organizes the form fields into logical sections */}
{/* Left Column */}
@@ -122,26 +123,25 @@ const FormComponent: React.FC = () => {
- {/* Form Footer */} + {/* Form Footer - contains actions for form submission and cancellation */}

Data in the import file is correct. Please press Continue to import.

- - + +
- - {formSubmitted &&
Success
} + {/* Success message - Display a success message if the form has been submitted successfully */} + {formSubmitted &&
Success
}
diff --git a/components/Tooltip/Tooltip.tsx b/components/Tooltip/Tooltip.tsx deleted file mode 100644 index 62fd481..0000000 --- a/components/Tooltip/Tooltip.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client" - -import * as RadixTooltip from "@radix-ui/react-tooltip" -import { cva, VariantProps } from "class-variance-authority" -import React from "react" -import { twMerge } from "tailwind-merge" - -const tooltipContent = cva([], { - variants: { - intent: { - primary: ["rounded-0.5md", "bg-zinc-700", "font-open-sans", "text-white"], - }, - size: { - md: ["px-4", "py-2.5", "text-2xs"], - }, - }, - defaultVariants: { - intent: "primary", - size: "md", - }, -}) - -const tooltipArrow = cva([], { - variants: { - intent: { - primary: ["fill-zinc-700"], - }, - size: { - md: ["w-4", "h-2"], - }, - }, - defaultVariants: { - intent: "primary", - size: "md", - }, -}) - -export interface TooltipProps extends VariantProps, RadixTooltip.TooltipProps { - explainer: React.ReactElement | string - children: React.ReactElement - className?: string - withArrow?: boolean - side?: "top" | "right" | "bottom" | "left" -} - -export function Tooltip({ - children, - explainer, - open, - defaultOpen, - onOpenChange, - intent, - size, - side = "top", - className, - withArrow, -}: TooltipProps) { - return ( - - - {children} - - - {explainer} - {withArrow ? : null} - - - - - ) -} diff --git a/e2e/form.spec.ts b/e2e/form.spec.ts new file mode 100644 index 0000000..2cdbd49 --- /dev/null +++ b/e2e/form.spec.ts @@ -0,0 +1,68 @@ +//TODO Install playwright + +import { expect, test } from "@playwright/test" + +//Form load and initial state validation +test("form loads correctly with initial state", async ({ page }) => { + await page.goto("/"); //Home URL + + // Check if specific fields are present and empty + await expect(page.locator('input[name="importName"]')).toBeEmpty(); + await expect(page.locator('input[name="manifestFile"]')).toBeEmpty(); + + await expect(page.locator('input[name="toleranceWindow"]')).not.toBeChecked(); + // Verify that no option is selected initially for "splitSchedule" + await expect(page.locator('input[name="splitSchedule"]:checked')).toHaveCount(0); + + // Verify that no option is selected initially for "client" + await expect(page.locator('input[name="client"]:checked')).toHaveCount(0); + + // Check for the presence of the submit button and that it's initially disabled + const submitButton = page.locator('button', { hasText: 'Continue Import' }); + await expect(submitButton).toBeDisabled(); + }); + + //File upload validation + test("handles file upload correctly", async ({ page }) => { + await page.goto("/"); + + // Simulate file upload + await page.setInputFiles('input[type="file"]', '/e2e/testFile.txt'); + + // Check if the file name is displayed correctly + await expect(page.locator('.file-info')).toHaveText(/testFile.txt/); + + // Optionally, check for the upload progress bar to reach 100% + await expect(page.locator('.upload-progress')).toHaveCSS('width', '100%'); + }); + + //Toggle and submit interactions + test("toggles a switch and submits the form", async ({ page }) => { + await page.goto("/"); + + // Toggle a switch + await page.locator('input[name="toleranceWindow"]').click(); + await expect(page.locator('input[name="toleranceWindow"]')).toBeChecked(); + + // Fill in other required fields + // Select an option from "importName" dropdown + await page.locator('select[name="importName"]').selectOption({ label: "Import ABC" }); + + // Upload a file for "manifestFile" + await page.locator('input[name="manifestFile"]').setInputFiles('/e2e/testFile.txt'); + + // Select an option for "splitSchedule" + await page.locator('input[name="splitSchedule"][value="Yes"]').click(); + + // Select an option for "client" + await page.locator('input[name="client"][value="Single"]').click(); + + // Enable and click the submit button + const submitButton = page.locator('button', { hasText: 'Continue Import' }); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + // Verify submission was successful + await expect(page.locator('.success-message')).toHaveText(/Success/); + }); + diff --git a/e2e/testFile.txt b/e2e/testFile.txt new file mode 100644 index 0000000..e4d7d11 --- /dev/null +++ b/e2e/testFile.txt @@ -0,0 +1 @@ +File to be used to test the file upload field \ No newline at end of file