This commit is contained in:
BIN
06_js-debug/projects/debugging-multi-step-form-template.zip
Normal file
BIN
06_js-debug/projects/debugging-multi-step-form-template.zip
Normal file
Binary file not shown.
24
06_js-debug/projects/debugging-multi-step-form-template/.gitignore
vendored
Normal file
24
06_js-debug/projects/debugging-multi-step-form-template/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,132 @@
|
||||
# Assignment: Completing the Multi-Step Wizard Form
|
||||
|
||||
In this assignment, you will complete a multi-step wizard form by adding two final steps, Step 4 (Personal Preferences) and Step 5 (Ready to Start), bringing the total number of steps to five. Your tasks include:
|
||||
|
||||
- Fixing any failing components so that all existing tests pass.
|
||||
- Adding two new steps with validation logic.
|
||||
- Writing tests to ensure the new steps are functional and robust.
|
||||
|
||||
Below are detailed requirements for the two new steps and their associated tests.
|
||||
|
||||
## Adding Step 4 and Step 5 to the Multi-Step Wizard Form
|
||||
|
||||
This section outlines the requirements for implementing Step 4: Personal Preferences and Step 5: Ready to Start. Your implementation should maintain consistency with the existing form design and functionality while ensuring robust validation and comprehensive test coverage.
|
||||
|
||||
### Step 4: Personal Preferences
|
||||
|
||||
This step gathers information about the user’s preferred activities and dietary preferences.
|
||||
|
||||
#### Inputs
|
||||
|
||||
- **Checkboxes**: Allow users to select one or more activities. Options include:
|
||||
- Yoga
|
||||
- Cardio
|
||||
- Strength Training
|
||||
- Meditation
|
||||
- **Dropdown (Select)**: Allow users to specify their dietary preferences. Options include:
|
||||
- None
|
||||
- Vegetarian
|
||||
- Vegan
|
||||
- Gluten-Free
|
||||
|
||||
Display the following text in this step: “Let us know your preferences to make the experience personal.”
|
||||
|
||||
#### What to Test
|
||||
|
||||
1. Rendering of Form Fields:
|
||||
|
||||
- **Activity Checkboxes**:
|
||||
- Verify that checkboxes for activities are rendered and grouped under the label “What type of activities do you prefer?”
|
||||
- Ensure that all activity options (Yoga, Cardio, Strength Training, Meditation) are present and selectable.
|
||||
- **Dietary Preferences Dropdown**:
|
||||
- Confirm that the dropdown for dietary preferences is rendered with the label “Do you have any dietary preferences?”
|
||||
- Ensure that all dietary options (None, Vegetarian, Vegan, Gluten-Free) are available for selection.
|
||||
|
||||
2. Validation of User Input:
|
||||
|
||||
- **Activity Preferences Validation**:
|
||||
- Attempt to proceed without selecting any activities and verify that an error message appears stating “At least one activity preference is required.”
|
||||
- Select one or more activities and ensure that the error message disappears upon valid input.
|
||||
- **Dietary Preferences Validation**:
|
||||
- Attempt to proceed without selecting a dietary preference and verify that an error message appears stating “Dietary preference is required.”
|
||||
- Select a dietary preference and ensure that the error message disappears upon valid input.
|
||||
|
||||
3. Dynamic Updates to Form Data:
|
||||
|
||||
- **Activity Checkboxes Interaction**:
|
||||
- Select and deselect activity checkboxes and confirm that the activityPreferences array in the form data updates accordingly.
|
||||
- **Dietary Preferences Dropdown Interaction**:
|
||||
- Change the selection in the dietary preferences dropdown and verify that the diet field in the form data updates to reflect the current selection.
|
||||
|
||||
#### Test Summary for Step 4: Personal Preferences
|
||||
|
||||
- **Test Case 1**: Verifies that all form fields (activity checkboxes and dietary preferences dropdown) are rendered correctly with the appropriate labels and options.
|
||||
- **Test Case 2**: Ensures validation errors appear when no activities or dietary preferences are selected and disappear when valid inputs are provided.
|
||||
- **Test Case 3**: Confirms that interacting with the form elements (selecting activities and dietary preferences) dynamically updates the form data.
|
||||
|
||||
### Step 5: Ready to Start
|
||||
|
||||
This step collects motivational information, enables users to join optional challenges, and allows them to choose their preferred reminder method.
|
||||
|
||||
#### Inputs
|
||||
|
||||
- **Short Text Input**: A field for users to describe what motivates them the most. Placeholder: “What motivates you the most?”
|
||||
- **Checkboxes**: Options to join one or more optional challenges, including:
|
||||
- Fitness Challenge
|
||||
- Healthy Eating Challenge
|
||||
- Mindfulness Challenge
|
||||
- **Dropdown (Select)**: A field to specify how users want to receive reminders. Options include:
|
||||
- Email
|
||||
- Push Notifications
|
||||
- None
|
||||
|
||||
Display the following text in this step: “We are ready to guide you! Set your preferences and begin your journey.”
|
||||
|
||||
#### What to Test
|
||||
|
||||
1. Rendering of Form Fields:
|
||||
|
||||
- **Motivational Input Field**:
|
||||
- Verify that the motivational input field is rendered with the label “What motivates you the most?” and the placeholder text is correctly displayed.
|
||||
- **Optional Challenges Checkboxes**:
|
||||
- Confirm that checkboxes for optional challenges are rendered and grouped under the label “Would you like to join any optional challenges?”
|
||||
- Ensure that all challenge options (Fitness Challenge, Healthy Eating Challenge, Mindfulness Challenge) are present and selectable.
|
||||
- **Reminders Dropdown**:
|
||||
- Check that the dropdown for reminder preferences is rendered with the label “Preferred way to receive reminders?”
|
||||
- Ensure that all reminder options (Email, Push Notifications, None) are available for selection.
|
||||
|
||||
2. Validation and Dynamic Updates of User Input:
|
||||
|
||||
- **Form Validation**:
|
||||
- Enter valid inputs for all fields:
|
||||
- Type a non-empty string into the motivation field.
|
||||
- Optionally select one or more challenges (since they are optional, the form should accept both selecting and not selecting any challenges).
|
||||
- Select a valid option from the reminders dropdown.
|
||||
- Ensure that the form passes validation without any errors when all inputs are valid.
|
||||
- **Dynamic Updates to Form Data**:
|
||||
- Confirm that typing into the motivation field updates the motivation field in the form data.
|
||||
- Verify that selecting or deselecting challenges updates the optionalChallenges array in the form data.
|
||||
- Ensure that changing the selection in the reminders dropdown updates the reminders field in the form data.
|
||||
|
||||
#### Test Summary for Step 5: Ready to Start
|
||||
|
||||
- **Test Case 1**: Verifies that all form fields (motivation input, optional challenges checkboxes, and reminders dropdown) are rendered correctly with the appropriate labels and options.
|
||||
- **Test Case 2**: Ensures the form passes validation with valid inputs and confirms that user interactions (typing in the motivation field, selecting/deselecting challenges, choosing a reminder option) dynamically update the form data accurately.
|
||||
|
||||
## License and terms of use
|
||||
|
||||
© Web Professional Institute Inc. All rights reserved.
|
||||
|
||||
This material is provided solely for students enrolled in courses offered by Web Professional Institute Inc. By accessing or using this code, you acknowledge that it is strictly for educational use within the context of Web Professional Institute Inc. programs.
|
||||
|
||||
**Usage Restrictions:**
|
||||
|
||||
- Redistribution, sharing, or copying of this material outside the course environment is strictly prohibited.
|
||||
- The content is designed to support your learning objectives and is not authorized for commercial projects, public repositories, or applications beyond the course scope.
|
||||
- Unauthorized commercial use or open-source distribution is strictly prohibited and may result in expulsion from the program and legal action.
|
||||
|
||||
**Agreement & Rights:**
|
||||
|
||||
As a student, you have agreed to these terms as part of your enrollment agreement, which includes additional details on permitted uses, restrictions, and policies. The author and Web Professional Institute Inc. retain all rights to this material, including the code base and instructional content.
|
||||
|
||||
For any questions regarding these terms, please contact Web Professional Institute Inc. for clarification.
|
||||
@@ -0,0 +1,32 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4580
06_js-debug/projects/debugging-multi-step-form-template/package-lock.json
generated
Normal file
4580
06_js-debug/projects/debugging-multi-step-form-template/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "debugging-assignment-wizard",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.6",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"autoprefixer": "^10.5.4",
|
||||
"eslint": "^10.9.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.4",
|
||||
"globals": "^17.11.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"postcss": "^8.5.26",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"vite": "^8.2.2",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7399/ingest/04244453-f432-4f3b-ade1-92bbdb574a0e',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'a04de5'},body:JSON.stringify({sessionId:'a04de5',runId:'post-fix',hypothesisId:'H3',location:'postcss.config.js',message:'postcss config loaded',data:{plugins:['tailwindcss','autoprefixer']},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import MultiStepForm from './MultiStepForm';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<MultiStepForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import ProgressIndicator from './components/ProgressIndicator';
|
||||
import NavigationButtons from './components/NavigationButtons';
|
||||
import AccountInfo from './steps/AccountInfo';
|
||||
import ActivityInfo from './steps/ActivityInfo';
|
||||
import GoalSetting from './steps/GoalSetting';
|
||||
|
||||
import { validate } from './utils/validation';
|
||||
|
||||
function MultiStepForm() {
|
||||
const [stepIndex, setStepIndex] = useState(1);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
activityLevel: 'never',
|
||||
energyLevel: 'low',
|
||||
goals: [],
|
||||
timeCommitment: '15',
|
||||
});
|
||||
|
||||
const formSteps = [
|
||||
{ id: 1, name: 'Account Info', component: AccountInfo },
|
||||
{ id: 2, name: 'Activity Info', component: ActivityInfo },
|
||||
{ id: 3, name: 'Goal Setting', component: GoalSetting },
|
||||
];
|
||||
|
||||
// ===== Event Handlers =====
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (field, id, isChecked) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: isChecked
|
||||
? [...prev[field], id]
|
||||
: prev[field].filter((item) => item !== id),
|
||||
}));
|
||||
};
|
||||
|
||||
// ===== Form Navigation =====
|
||||
const handleNext = () => {
|
||||
if (handleValidation()) {
|
||||
setStepIndex(stepIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setErrors({});
|
||||
setStepIndex(stepIndex - 1);
|
||||
};
|
||||
|
||||
// ===== Form Validation & Submission =====
|
||||
const handleValidation = () => {
|
||||
const validationErrors = validate(formData, stepIndex);
|
||||
setErrors(validationErrors);
|
||||
return Object.keys(validationErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (handleValidation()) {
|
||||
alert('Form submitted successfully!');
|
||||
console.log('Submitted Data:', formData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<div className="pt-32 mx-4">
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow max-w-[600px] mx-auto">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<h1 className="text-2xl font-bold text-center my-6">
|
||||
Let's get you started!
|
||||
</h1>
|
||||
|
||||
<ProgressIndicator steps={formSteps} currentStep={stepIndex} />
|
||||
|
||||
<form onSubmit={handleSubmit} className="relative mt-10">
|
||||
{formSteps.map((s) => {
|
||||
const StepComponent = s.component;
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`transition-opacity duration-500 ${
|
||||
stepIndex === s.id
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 absolute w-full'
|
||||
}`}
|
||||
aria-hidden={stepIndex !== s.id}
|
||||
>
|
||||
{stepIndex === s.id && (
|
||||
<StepComponent
|
||||
formData={formData}
|
||||
handleChange={handleChange}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
errors={errors}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<NavigationButtons
|
||||
currentStep={stepIndex}
|
||||
totalSteps={formSteps.length}
|
||||
handlePrev={handlePrev}
|
||||
handleNext={handleNext}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultiStepForm;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
const FormCheckboxes = ({ legend, required, options, onChange, error }) => {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm/6 font-medium text-gray-900">
|
||||
{legend} {required && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
|
||||
<fieldset className="mt-4">
|
||||
<legend className="sr-only">{legend}</legend>
|
||||
<div className="space-y-5">
|
||||
{options.map((option) => (
|
||||
<div key={option.id} className="relative flex items-start">
|
||||
<div className="flex h-6 items-center">
|
||||
<input
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
type="checkbox"
|
||||
aria-describedby={`${option.id}-description`}
|
||||
className="size-4 rounded border-gray-300 text-teal-600 focus:ring-teal-600"
|
||||
onChange={(e) => onChange(option.id, e.target.checked)}
|
||||
checked={option.checked}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label
|
||||
htmlFor={option.id}
|
||||
className="font-medium text-gray-900"
|
||||
>
|
||||
{option.label}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<p className="mt-2 text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormCheckboxes;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
const FormInput = ({
|
||||
htmlFor,
|
||||
label,
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
placeholder,
|
||||
required = false,
|
||||
type = 'text',
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
className="block text-sm/6 font-medium text-gray-900"
|
||||
>
|
||||
{label} {required && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
|
||||
<div className="mt-2">
|
||||
<input
|
||||
type={type}
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={`block w-full rounded-md border-0 py-1.5 px-2.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm/6 ${
|
||||
error
|
||||
? 'text-red-900 ring-red-400 placeholder:text-red-300 focus:ring-red-500'
|
||||
: 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-teal-600'
|
||||
}`}
|
||||
placeholder={placeholder}
|
||||
aria-required={required}
|
||||
aria-invalid={error ? 'true' : 'false'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormInput;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
const FormSelect = ({
|
||||
id,
|
||||
name,
|
||||
options,
|
||||
defaultValue,
|
||||
onChange,
|
||||
label,
|
||||
required = false,
|
||||
className = '',
|
||||
labelClassName = '',
|
||||
error,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={`block text-sm font-medium text-gray-900 ${labelClassName}`}
|
||||
>
|
||||
{label} {required && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
id={id}
|
||||
name={name}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
className={`mt-2 block w-full rounded-md border-0 py-2.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-teal-600 sm:text-sm ${className}`}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<option key={index} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<p className="mt-2 text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormSelect;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
|
||||
function NavigationButtons({
|
||||
currentStep,
|
||||
totalSteps,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
handleSubmit,
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-between mt-10">
|
||||
{currentStep > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrev}
|
||||
className="px-8 py-2.5 bg-gray-200 text-gray-700 text-sm font-semibold rounded-md hover:bg-gray-300 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
)}
|
||||
{currentStep < totalSteps && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
className="ml-auto px-11 py-2.5 bg-teal-600 text-sm font-semibold text-white shadow-sm rounded-md hover:bg-teal-700 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
)}
|
||||
{currentStep === totalSteps && (
|
||||
<button
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
className="ml-auto px-10 py-2.5 bg-teal-600 text-sm font-semibold text-white shadow-sm rounded-md hover:bg-teal-700 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavigationButtons;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
function ProgressIndicator({ steps, currentStep }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex-1">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
currentStep >= step.id
|
||||
? 'bg-teal-600 text-white'
|
||||
: 'bg-gray-300 text-gray-700'
|
||||
}`}
|
||||
aria-current={currentStep === step.id ? 'step' : undefined}
|
||||
>
|
||||
{step.id}
|
||||
</div>
|
||||
{index !== steps.length - 1 && (
|
||||
<div
|
||||
className={`flex-1 h-1 ${
|
||||
currentStep > step.id ? 'bg-teal-600' : 'bg-gray-300'
|
||||
}`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProgressIndicator;
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
import FormInput from '../components/FormInput';
|
||||
|
||||
function AccountInfo({ formData, handleChange, errors }) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Welcome to BrightPath Wellness!
|
||||
</h2>
|
||||
<p className="mb-6 text-gray-700">
|
||||
Let’s create your account and begin your wellness journey.
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<FormInput
|
||||
htmlFor="fullName"
|
||||
label="Full Name"
|
||||
type="text"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
value={''}
|
||||
onChange={handleChange}
|
||||
error={errors.fullName}
|
||||
placeholder="Enter your full name"
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<FormInput
|
||||
htmlFor="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
placeholder="Enter your email address"
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<FormInput
|
||||
htmlFor="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
placeholder="Create a password"
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountInfo;
|
||||
@@ -0,0 +1,45 @@
|
||||
import FormSelect from '../components/FormSelect';
|
||||
|
||||
function ActivityInfo({ handleChange, errors }) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Activity & Energy</h2>
|
||||
<p className="mb-6 text-gray-700">
|
||||
Tell us a little about your activity level and energy.
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<FormSelect
|
||||
id="activityLevel"
|
||||
name="activityLevel"
|
||||
options={[
|
||||
{ value: 'never', label: 'Never' },
|
||||
{ value: 'rarely', label: 'Rarely' },
|
||||
{ value: '1-2 times a week', label: '1-2 times a week' },
|
||||
{ value: '3+ times a week', label: '3+ times a week' },
|
||||
]}
|
||||
onChange={handleChange}
|
||||
required={true}
|
||||
error={errors.activityLevel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<FormSelect
|
||||
id="energyLevel"
|
||||
name="energyLevel"
|
||||
options={[
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
]}
|
||||
onChange={handleChange}
|
||||
required={true}
|
||||
error={errors.energyLevel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActivityInfo;
|
||||
@@ -0,0 +1,65 @@
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import FormCheckboxes from '../components/FormCheckboxes';
|
||||
|
||||
function GoalSetting({ formData, handleChange, handleCheckboxChange, errors }) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Activity & Energy</h2>
|
||||
<p className="mb-6 text-gray-700">
|
||||
Tell us a little about your activity level and energy.
|
||||
</p>
|
||||
|
||||
<div className="mb-10">
|
||||
<FormCheckboxes
|
||||
legend="What are your goals?"
|
||||
options={[
|
||||
{
|
||||
id: 'weightLoss',
|
||||
name: 'goals',
|
||||
label: 'Weight Loss',
|
||||
checked: formData.goals.includes('weightLoss'),
|
||||
},
|
||||
{
|
||||
id: 'muscleGain',
|
||||
name: 'goals',
|
||||
label: 'Muscle Gain',
|
||||
checked: formData.goals.includes('musleGain'),
|
||||
},
|
||||
{
|
||||
id: 'improveHealth',
|
||||
name: 'goals',
|
||||
label: 'Improve Health',
|
||||
checked: formData.goals.includes('improveHealt'),
|
||||
},
|
||||
{
|
||||
id: 'boostEnergy',
|
||||
name: 'goals',
|
||||
label: 'Boost Energy',
|
||||
checked: formData.goals.includes('boostEnergy'),
|
||||
},
|
||||
]}
|
||||
onChange={(id, isChecked) => handleCheckboxChange('goals', id)}
|
||||
required={true}
|
||||
error={errors.goals}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<FormSelect
|
||||
id="timeCommitment"
|
||||
name="timeCommitment"
|
||||
options={[
|
||||
{ value: '15', label: '15 minutes' },
|
||||
{ value: '30', label: '30 minutes' },
|
||||
{ value: '60', label: '1 hour' },
|
||||
]}
|
||||
onChange={handleChange}
|
||||
label="How much time can you commit each day?"
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GoalSetting;
|
||||
@@ -0,0 +1,96 @@
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const PASSWORD_MIN_LENGTH = 6;
|
||||
|
||||
// Helper Functions
|
||||
export const isEmpty = (value) => {
|
||||
return !value || !value.trim();
|
||||
};
|
||||
|
||||
export const isValidEmail = (email) => {
|
||||
return EMAIL_REGEX.test(email);
|
||||
};
|
||||
|
||||
export const hasMinLength = (value, min) => {
|
||||
return value.length >= min;
|
||||
};
|
||||
|
||||
export const hasTwoWords = (fullName) => {
|
||||
return fullName.trim().split(' ').length >= 2;
|
||||
};
|
||||
|
||||
// Main Validation Function
|
||||
export const validate = (formData, currentStep) => {
|
||||
const errors = {};
|
||||
|
||||
const {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
activityLevel,
|
||||
energyLevel,
|
||||
goals,
|
||||
timeCommitment,
|
||||
} = formData;
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
// Full Name Validations
|
||||
if (isEmpty(fullName)) {
|
||||
errors.fullName = 'Full Name is required';
|
||||
} else if (!hasTwoWords(fullName)) {
|
||||
errors.fullName = 'Full Name must contain at least two words';
|
||||
}
|
||||
|
||||
// Email Validations
|
||||
if (isEmpty(email)) {
|
||||
errors.email = 'Email is required';
|
||||
} else if (!isValidEmail(email)) {
|
||||
errors.email = 'Email address is invalid';
|
||||
}
|
||||
|
||||
// Password Validations
|
||||
if (isEmpty(password)) {
|
||||
errors.password = 'Password is required';
|
||||
} else {
|
||||
if (!hasMinLength(password, PASSWORD_MIN_LENGTH)) {
|
||||
errors.password = `Password must be at least ${PASSWORD_MIN_LENGTH} characters`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Activity Level Validation
|
||||
if (isEmpty(activityLevel)) {
|
||||
errors.activityLevel = 'Activity Level is required';
|
||||
}
|
||||
|
||||
// Energy Level Validation
|
||||
if (isEmpty(energyLevel)) {
|
||||
errors.energyLevel = 'Energy Level is required';
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Goals Validation
|
||||
if (!Array.isArray(goals) || goals.length === 0) {
|
||||
errors.goals = 'At least one goal is required';
|
||||
} else {
|
||||
goals.forEach((goal, index) => {
|
||||
if (isEmpty(goal)) {
|
||||
errors[`goals.${index}`] = 'Goal cannot be empty';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Time Commitment Validation
|
||||
if (isEmpty(timeCommitment)) {
|
||||
errors.timeCommitment = 'Time Commitment is required';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import forms from '@tailwindcss/forms';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [forms],
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { it, expect, describe, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import AccountInfo from '../../src/steps/AccountInfo';
|
||||
import { validate } from '../../src/utils/validation';
|
||||
|
||||
describe('AccountInfo Component', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
const step = 1;
|
||||
|
||||
it('renders all form fields', () => {
|
||||
render(
|
||||
<AccountInfo
|
||||
formData={{ fullName: '', email: '', password: '' }}
|
||||
handleChange={() => {}}
|
||||
errors={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(/Full Name/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Email/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays validation errors for invalid data', () => {
|
||||
const formData = { fullName: '', email: '', password: '' };
|
||||
const errors = validate(formData, step);
|
||||
|
||||
render(
|
||||
<AccountInfo
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Full Name is required/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Email is required/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Password is required/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays no validation errors for valid data', () => {
|
||||
const formData = {
|
||||
fullName: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
password: 'securepassword',
|
||||
};
|
||||
const errors = validate(formData, step);
|
||||
|
||||
render(
|
||||
<AccountInfo
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/is required/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/invalid/i)).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/must be at least 6 characters/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls handleChange and updates error messages dynamically', async () => {
|
||||
let formData = { fullName: '', email: '', password: '' };
|
||||
let errors = validate(formData, step);
|
||||
|
||||
const handleChangeTest = (field, value) => {
|
||||
formData = { ...formData, [field]: value };
|
||||
errors = validate(formData, step);
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<AccountInfo
|
||||
formData={formData}
|
||||
handleChange={({ target }) =>
|
||||
handleChangeTest(target.name, target.value)
|
||||
}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
const fullNameInput = screen.getByLabelText(/Full Name/i);
|
||||
await userEvent.type(fullNameInput, 'John');
|
||||
|
||||
rerender(
|
||||
<AccountInfo
|
||||
formData={formData}
|
||||
handleChange={({ target }) =>
|
||||
handleChangeTest(target.name, target.value)
|
||||
}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Full Name must contain at least two words/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('retains user input in form fields', () => {
|
||||
const formData = {
|
||||
fullName: 'Jane Doe',
|
||||
email: 'jane.doe@example.com',
|
||||
password: 'securepassword',
|
||||
};
|
||||
const errors = validate(formData, step);
|
||||
|
||||
render(
|
||||
<AccountInfo
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(/Full Name/i)).toHaveValue('Jane Doe');
|
||||
expect(screen.getByLabelText(/Email/i)).toHaveValue('jane.doe@example.com');
|
||||
expect(screen.getByLabelText(/Password/i)).toHaveValue('securepassword');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { it, expect, describe, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import ActivityInfo from '../../src/steps/ActivityInfo';
|
||||
import { validate } from '../../src/utils/validation';
|
||||
|
||||
describe('ActivityInfo Component', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
const step = 2;
|
||||
|
||||
it('renders all form fields', () => {
|
||||
render(<ActivityInfo handleChange={() => {}} errors={{}} />);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText(/How often do you exercise\?/i)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText(
|
||||
/How would you describe your average energy level\?/i
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays validation errors for invalid data', () => {
|
||||
const formData = {
|
||||
fullName: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
activityLevel: '',
|
||||
energyLevel: '',
|
||||
goals: [],
|
||||
timeCommitment: '15',
|
||||
};
|
||||
const errors = validate(formData, step);
|
||||
|
||||
render(<ActivityInfo handleChange={() => {}} errors={errors} />);
|
||||
|
||||
expect(screen.getByText(/Activity Level is required/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Energy Level is required/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays no validation errors for valid data', () => {
|
||||
const formData = {
|
||||
fullName: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
activityLevel: 'moderate',
|
||||
energyLevel: 'high',
|
||||
goals: [],
|
||||
timeCommitment: '15',
|
||||
};
|
||||
const errors = validate(formData, step);
|
||||
|
||||
render(<ActivityInfo handleChange={() => {}} errors={errors} />);
|
||||
|
||||
expect(screen.queryByText(/is required/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls handleChange and updates error messages dynamically', async () => {
|
||||
let formData = {
|
||||
fullName: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
activityLevel: '',
|
||||
energyLevel: '',
|
||||
goals: [],
|
||||
timeCommitment: '15',
|
||||
};
|
||||
let errors = validate(formData, step);
|
||||
|
||||
const handleChangeTest = (field, value) => {
|
||||
formData = { ...formData, [field]: value };
|
||||
errors = validate(formData, step);
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<ActivityInfo
|
||||
handleChange={({ target }) =>
|
||||
handleChangeTest(target.name, target.value)
|
||||
}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
const activitySelect = screen.getByLabelText(
|
||||
/How often do you exercise\?/i
|
||||
);
|
||||
const energySelect = screen.getByLabelText(
|
||||
/How would you describe your average energy level\?/i
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(activitySelect, '3+ times a week');
|
||||
await userEvent.selectOptions(energySelect, 'medium');
|
||||
|
||||
rerender(
|
||||
<ActivityInfo
|
||||
handleChange={({ target }) =>
|
||||
handleChangeTest(target.name, target.value)
|
||||
}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByText(/Activity Level is required/i)
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Energy Level is required/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import { it, expect, describe, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import GoalSetting from '../../src/steps/GoalSetting';
|
||||
import { validate } from '../../src/utils/validation';
|
||||
|
||||
describe('GoalSetting Component', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
const step = 3;
|
||||
|
||||
it('renders all form fields', () => {
|
||||
render(
|
||||
<GoalSetting
|
||||
formData={{ goals: [], timeCommitment: '' }}
|
||||
handleChange={() => {}}
|
||||
handleCheckboxChange={() => {}}
|
||||
errors={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByRole to target the fieldset for "What are your goals?"
|
||||
expect(
|
||||
screen.getByRole('group', { name: /What are your goals\?/i })
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByLabelText(/How much time can you commit each day\?/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays validation errors for empty fields', () => {
|
||||
const formData = {
|
||||
goals: [],
|
||||
timeCommitment: '',
|
||||
};
|
||||
const errors = validate(formData, step);
|
||||
|
||||
render(
|
||||
<GoalSetting
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
handleCheckboxChange={() => {}}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/At least one goal is required/i)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Time Commitment is required/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays no validation errors for valid data', () => {
|
||||
const formData = {
|
||||
goals: ['weightLoss', 'muscleGain'],
|
||||
timeCommitment: '30',
|
||||
};
|
||||
const errors = validate(formData, step);
|
||||
|
||||
render(
|
||||
<GoalSetting
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
handleCheckboxChange={() => {}}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/is required/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles checkbox interactions and updates goals dynamically', async () => {
|
||||
let formData = { goals: [], timeCommitment: '' };
|
||||
let errors = validate(formData, step);
|
||||
|
||||
const handleCheckboxChangeTest = (field, id, isChecked) => {
|
||||
const updatedGoals = isChecked
|
||||
? [...formData[field], id]
|
||||
: formData[field].filter((goal) => goal !== id);
|
||||
|
||||
formData = { ...formData, [field]: updatedGoals };
|
||||
errors = validate(formData, step);
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<GoalSetting
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
handleCheckboxChange={handleCheckboxChangeTest}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
const weightLossCheckbox = screen.getByLabelText(/Weight Loss/i);
|
||||
const muscleGainCheckbox = screen.getByLabelText(/Muscle Gain/i);
|
||||
|
||||
await userEvent.click(weightLossCheckbox);
|
||||
rerender(
|
||||
<GoalSetting
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
handleCheckboxChange={handleCheckboxChangeTest}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(weightLossCheckbox).toBeChecked();
|
||||
expect(
|
||||
screen.queryByText(/At least one goal is required/i)
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(muscleGainCheckbox);
|
||||
rerender(
|
||||
<GoalSetting
|
||||
formData={formData}
|
||||
handleChange={() => {}}
|
||||
handleCheckboxChange={handleCheckboxChangeTest}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(muscleGainCheckbox).toBeChecked();
|
||||
expect(formData.goals).toContain('weightLoss');
|
||||
expect(formData.goals).toContain('muscleGain');
|
||||
});
|
||||
|
||||
it('handles select interactions and updates time commitment dynamically', async () => {
|
||||
let formData = { goals: ['weightLoss'], timeCommitment: '' };
|
||||
let errors = validate(formData, step);
|
||||
|
||||
const handleChangeTest = (field, value) => {
|
||||
formData = { ...formData, [field]: value };
|
||||
errors = validate(formData, step);
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<GoalSetting
|
||||
formData={formData}
|
||||
handleChange={({ target }) =>
|
||||
handleChangeTest(target.name, target.value)
|
||||
}
|
||||
handleCheckboxChange={() => {}}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
const timeCommitmentSelect = screen.getByLabelText(
|
||||
/How much time can you commit each day\?/i
|
||||
);
|
||||
await userEvent.selectOptions(timeCommitmentSelect, '45');
|
||||
|
||||
rerender(
|
||||
<GoalSetting
|
||||
formData={formData}
|
||||
handleChange={({ target }) =>
|
||||
handleChangeTest(target.name, target.value)
|
||||
}
|
||||
handleCheckboxChange={() => {}}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(timeCommitmentSelect).toHaveValue('30');
|
||||
expect(
|
||||
screen.queryByText(/Time Commitment is required/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
isEmpty,
|
||||
isValidEmail,
|
||||
hasMinLength,
|
||||
hasTwoWords,
|
||||
} from '../../src/utils/validation';
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('isEmpty', () => {
|
||||
it('should return true for empty strings', () => {
|
||||
expect(isEmpty('')).toBe(true);
|
||||
expect(isEmpty(' ')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-empty strings', () => {
|
||||
expect(isEmpty('John')).toBe(false);
|
||||
expect(isEmpty(' John Doe ')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidEmail', () => {
|
||||
it('should validate correct email formats', () => {
|
||||
expect(isValidEmail('john@example.com')).toBe(true);
|
||||
expect(isValidEmail('jane.doe@sub.domain.co')).toBe(true);
|
||||
});
|
||||
|
||||
it('should invalidate incorrect email formats', () => {
|
||||
expect(isValidEmail('johnexample.com')).toBe(false);
|
||||
expect(isValidEmail('jane@.com')).toBe(false);
|
||||
expect(isValidEmail('jane@com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMinLength', () => {
|
||||
it('should return true if value meets minimum length', () => {
|
||||
expect(hasMinLength('password', 6)).toBe(true);
|
||||
expect(hasMinLength('123456', 6)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if value does not meet minimum length', () => {
|
||||
expect(hasMinLength('pass', 6)).toBe(false);
|
||||
expect(hasMinLength('123', 6)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasTwoWords', () => {
|
||||
it('should return true if fullName has at least two words', () => {
|
||||
expect(hasTwoWords('John Doe')).toBe(true);
|
||||
expect(hasTwoWords('Jane Mary Doe')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if fullName has less than two words', () => {
|
||||
expect(hasTwoWords('John')).toBe(false);
|
||||
expect(hasTwoWords(' Jane ')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7399/ingest/04244453-f432-4f3b-ade1-92bbdb574a0e',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'a04de5'},body:JSON.stringify({sessionId:'a04de5',runId:'post-fix',hypothesisId:'H3',location:'vite.config.js',message:'vite config loaded',data:{hasReactPlugin:true},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
plugins: [react()],
|
||||
});
|
||||
3964
06_js-debug/uebungen/u17-19_shop-vitest/package-lock.json
generated
Normal file
3964
06_js-debug/uebungen/u17-19_shop-vitest/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,14 +20,14 @@
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.5",
|
||||
"@testing-library/user-event": "^14.6.6",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
||||
"@typescript-eslint/parser": "^8.67.0",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint": "^10.9.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.4",
|
||||
"jsdom": "^30.0.1",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { it, expect, describe } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import CategorySection from '../components/CategorySection';
|
||||
import { fakeCategories } from './__mocks__/categories.mock';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const categories = fakeCategories;
|
||||
|
||||
describe('CategorySection', () => {
|
||||
it('should render the heading right', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<CategorySection categories={categories} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
screen.debug();
|
||||
|
||||
expect(screen.getByText(/Explore by Category/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have 3 links and 3 pics', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<CategorySection categories={categories} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const imgs = screen.getAllByRole('img');
|
||||
const links = screen.getAllByRole('link');
|
||||
|
||||
expect(imgs).toHaveLength(3); //.length()
|
||||
expect(links).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { it, expect, describe } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { fakeNavItems } from './__mocks__/navItems.mock';
|
||||
|
||||
import Footer from '../components/Footer';
|
||||
|
||||
const navItems = fakeNavItems;
|
||||
|
||||
describe('Footer', () => {
|
||||
it('should render the navigation items in the footer', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Footer navigation={navItems} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
screen.debug();
|
||||
|
||||
navItems.forEach((item) => {
|
||||
const spanEl = screen.getByText(item.name);
|
||||
const linkEl = spanEl.parentElement;
|
||||
expect(spanEl).toBeInTheDocument();
|
||||
expect(linkEl).toHaveAttribute('href', item.href);
|
||||
});
|
||||
|
||||
// expect(screen.getByText(navItems[0].name)).toBeInTheDocument();
|
||||
// expect(screen.getByText(navItems[1].name)).toBeInTheDocument();
|
||||
// expect(screen.getByText(navItems[2].name)).toBeInTheDocument();
|
||||
// expect(screen.getByText(navItems[3].name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -57,4 +57,49 @@ describe('ImageGallery', () => {
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/3.png',
|
||||
);
|
||||
});
|
||||
|
||||
// Aro
|
||||
it('should display the second image when the arrow key right is pressed', async () => {
|
||||
const { images } = renderComponent();
|
||||
const mainImage = images[images.length - 1];
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
expect(mainImage).toHaveAttribute('src', product.images[0]);
|
||||
|
||||
await user.keyboard('{ArrowRight}');
|
||||
|
||||
expect(mainImage).toBeInTheDocument();
|
||||
expect(mainImage).toHaveAttribute('src', product.images[1]);
|
||||
});
|
||||
|
||||
it('should display the first image wenn pressing arrow key right and than arrow key left', async () => {
|
||||
const { images } = renderComponent();
|
||||
const mainImage = images[images.length - 1];
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
expect(mainImage).toHaveAttribute('src', product.images[0]);
|
||||
await user.keyboard('{ArrowRight}');
|
||||
|
||||
expect(mainImage).toHaveAttribute('src', product.images[1]);
|
||||
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
|
||||
expect(mainImage).toBeInTheDocument();
|
||||
expect(mainImage).toHaveAttribute('src', product.images[0]);
|
||||
});
|
||||
|
||||
it('should display last image after pressing arrow key left', async () => {
|
||||
const { images } = renderComponent();
|
||||
const mainImage = images[images.length - 1];
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
expect(mainImage).toHaveAttribute('src', product.images[0]);
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
|
||||
expect(mainImage).toBeInTheDocument();
|
||||
expect(mainImage).toHaveAttribute('src', product.images[product.images.length - 1]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const fakeCategories = [
|
||||
{
|
||||
imgSrc: 'https://images.unsplash.com/photo-1530011940847-04cba36df39f?q=80&w=1920',
|
||||
imgAlt: 'Two female models sitting on a bed. Photo by Billie (@billiebodybrand) on Unsplash.',
|
||||
href: '/women',
|
||||
linkText: 'Women Clothing',
|
||||
},
|
||||
{
|
||||
imgSrc: 'https://images.unsplash.com/photo-1595909315417-2edd382a56dc?q=80&w=1920',
|
||||
imgAlt:
|
||||
'Jumping rope, smartphone, sports bra, a pair of sneakers, and a water bottle. Photo by erica steeves on Unsplash.',
|
||||
href: '/accessories',
|
||||
linkText: 'Accessories',
|
||||
},
|
||||
{
|
||||
imgSrc: 'https://images.unsplash.com/photo-1467779009031-53938b78ca38?q=80&w=1920',
|
||||
imgAlt: 'Group of men sitting in a van. Photo by Luke Porter on Unsplash.',
|
||||
href: '/men',
|
||||
linkText: 'Men Clothing',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
export const fakeNavItems = [
|
||||
{
|
||||
name: 'Home 1',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
name: 'Women 1',
|
||||
href: '/women1',
|
||||
},
|
||||
{
|
||||
name: 'Men 1',
|
||||
href: '/men1',
|
||||
},
|
||||
{
|
||||
name: 'Accessories 1',
|
||||
href: '/accessories1',
|
||||
},
|
||||
];
|
||||
4459
06_js-debug/unterricht/tag50/01_shop-vitest/package-lock.json
generated
Normal file
4459
06_js-debug/unterricht/tag50/01_shop-vitest/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,18 +20,18 @@
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.5",
|
||||
"@testing-library/user-event": "^14.6.6",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
||||
"@typescript-eslint/parser": "^8.67.0",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint": "^10.9.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.4",
|
||||
"jsdom": "^30.0.1",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.2.2",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('Hero', () => {
|
||||
it('should render the company name when passed', () => {
|
||||
render(<Hero companyName="My Company" />);
|
||||
|
||||
screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
|
||||
// screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
|
||||
|
||||
// Arrange
|
||||
const heading = screen.getByRole('heading', { level: 1 });
|
||||
@@ -30,7 +30,7 @@ describe('Hero', () => {
|
||||
it('should render the company name Vogue Junction as default', () => {
|
||||
render(<Hero />);
|
||||
|
||||
screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
|
||||
// screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
|
||||
|
||||
// Arrange
|
||||
const heading = screen.getByRole('heading', { level: 1 });
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { add } from '../utils/helpers';
|
||||
|
||||
// Test Suite
|
||||
describe('add', () => {
|
||||
// Test Case
|
||||
it('should return 3 when 2 and 1 are passed', () => {
|
||||
// AAA
|
||||
expect(add(2, 1)).toBe(3);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user