Skip to content

Commit

Permalink
setup envirnoment
Browse files Browse the repository at this point in the history
implement HSButton

implement HSInput

mend

implement login design

resolve eslint errors

rebase from develop

fix hovering styles  button

rebase form develop

set up formik

implement stage 1 of valifation usin formik

Reducing boilerplate

reduce duplicate codes

rebase from develop

complete form validatio

remove eslint error & initials unit tests

abort all written tests

working on lints

update eslint file

fix test errors

rebase from develop

implement Login component
  • Loading branch information
wayneleon1 committed Jun 27, 2024
1 parent 4c0c5b6 commit 4b4b241
Show file tree
Hide file tree
Showing 14 changed files with 1,689 additions and 789 deletions.
10 changes: 9 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,18 @@ module.exports = {
'import/no-extraneous-dependencies': 0,
'import/extensions': 0,
'react/require-default-props': 0,
'react/self-closing-comp': 0,
'react/jsx-props-no-spreading': 0,
'@typescript-eslint/no-explicit-any': 0,
'no-param-reassign': [
'error',
{ props: true, ignorePropertyModificationsFor: ['state'] },
],
},
ignorePatterns: ['dist/**/*', 'postcss.config.js', 'tailwind.config.js'],
ignorePatterns: [
'dist/*/',
'postcss.config.js',
'tailwind.config.js',
'vite.config.ts',
],
};
1,842 changes: 1,060 additions & 782 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
"dotenv": "^16.4.5",
"formik": "^2.4.6",
"history": "^5.3.0",
"hero-slider": "^3.2.1",
"jest": "^29.7.0",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.2.1",
"react-loader-spinner": "^6.1.6",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1",
"react-spinners": "^0.14.1",
Expand All @@ -44,6 +48,7 @@
"@types/react-dom": "^18.2.22",
"@types/redux-mock-store": "^1.0.6",
"@types/testing-library__react": "^10.2.0",
"@types/jwt-decode": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"@vitejs/plugin-react": "^4.2.1",
Expand Down
209 changes: 209 additions & 0 deletions src/__test__/signInSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import {
render,
screen,
fireEvent,
waitFor,
cleanup,
} from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import signInReducer, { loginUser, logout } from '@/features/Auth/SignInSlice';
import SignIn from '@/pages/SignIn';

const createTestStore = () =>
configureStore({ reducer: { signIn: signInReducer } });
let store: any;

const renderSignIn = () => {
render(
<Provider store={store}>
<MemoryRouter initialEntries={['/signin']}>
<Routes>
<Route path="/signin" element={<SignIn />} />
</Routes>
</MemoryRouter>
</Provider>
);
};

describe('signInSlice', () => {
beforeEach(() => {
store = createTestStore();
});

vi.mock('jwt-decode', () => ({
jwtDecode: () => ({
user: {
userType: {
name: 'Admin',
},
},
}),
}));

it('should handle initial state', () => {
const { signIn } = store.getState();
expect(signIn).toEqual({
token: null,
loading: false,
error: null,
message: null,
role: null,
needsVerification: false,
needs2FA: false,
});
});

it('should handle loginUser.pending', () => {
const action = { type: loginUser.pending.type };
const state = signInReducer(undefined, action);
expect(state).toEqual({
token: null,
loading: true,
error: null,
message: null,
role: null,
needsVerification: false,
needs2FA: false,
});
});

it('should handle loginUser.fulfilled', () => {
const action = {
type: loginUser.fulfilled.type,
payload: { token: 'testToken', message: 'Login successful' },
};
const state = signInReducer(undefined, action);
expect(state).toEqual({
token: 'testToken',
loading: false,
error: null,
message: 'Login successful',
role: 'Admin',
needsVerification: false,
needs2FA: false,
});
});

it('should handle loginUser.rejected', () => {
const action = {
type: loginUser.rejected.type,
payload: { message: 'Login failed' },
};
const state = signInReducer(undefined, action);
expect(state).toEqual({
token: null,
loading: false,
error: 'Login failed',
message: null,
role: null,
needsVerification: false,
needs2FA: false,
});
});

it('should handle logout', () => {
const initialState = {
token: 'testToken',
loading: false,
error: null,
message: 'Login successful',
role: 'Admin',
needsVerification: false,
needs2FA: false,
};
const action = { type: logout.type };
const state = signInReducer(initialState, action);
expect(state).toEqual({
token: null,
loading: false,
error: null,
message: 'Logout Successfully',
role: null,
needsVerification: false,
needs2FA: false,
});
});
});

describe('SignIn Component', () => {
beforeEach(() => {
store = createTestStore();
renderSignIn();
});

afterEach(() => {
cleanup();
});

it('renders SignIn Title', () => {
expect(screen.getByTestId('title')).toBeInTheDocument();
});

it('renders Email Input Field', () => {
expect(screen.getByPlaceholderText('Enter your email')).toBeInTheDocument();
});

it('renders Password Input Field', () => {
expect(
screen.getByPlaceholderText('Enter your password')
).toBeInTheDocument();
});

it('renders the form and allows user to fill out and submit', async () => {
const emailInput = screen.getByPlaceholderText('Enter your email');
const passwordInput = screen.getByPlaceholderText('Enter your password');
const submitButton = screen.getByText(/Sign In/);

fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);

await waitFor(() => {
expect(screen.queryByText(/Loading.../i)).toBeInTheDocument();
});
});

it('displays error messages for invalid input', async () => {
const emailInput = screen.getByPlaceholderText('Enter your email');
const passwordInput = screen.getByPlaceholderText('Enter your password');
const submitButton = screen.getByText(/Sign In/);

fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
fireEvent.change(passwordInput, { target: { value: '123' } });
fireEvent.click(submitButton);

await waitFor(() => {
expect(screen.getByText(/Invalid email format/i)).toBeInTheDocument();
expect(
screen.getByText(/password must contain at least 6 chars/i)
).toBeInTheDocument();
});
});

it('does not submit the form with incomplete user details', async () => {
const submitButton = screen.getByText(/Sign In/);
fireEvent.click(submitButton);

await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
});

it('submits the form successfully', async () => {
const emailInput = screen.getByPlaceholderText('Enter your email');
const passwordInput = screen.getByPlaceholderText('Enter your password');
const submitButton = screen.getByText(/Sign In/);

fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);

await waitFor(() => {
expect(screen.queryByText(/Loading.../i)).not.toBeInTheDocument();
});
});
});
2 changes: 2 additions & 0 deletions src/app/store.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import signUpReducer from '../features/Auth/SignUpSlice';
import signInReducer from '../features/Auth/SignInSlice';

export const store = configureStore({
reducer: {
signUp: signUpReducer,
signIn: signInReducer,
},
});

Expand Down
Empty file removed src/components/button
Empty file.
37 changes: 37 additions & 0 deletions src/components/form/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Link } from 'react-router-dom';

interface MyButtonProps {
path?: string;
title: string | JSX.Element;
styles?: string;
onClick?: () => void;
icon?: JSX.Element;
target?: '_blank' | '_self' | '_parent' | '_top';
onChange?: React.ChangeEventHandler<HTMLAnchorElement>;
}

function HSButton({
path,
onClick,
title,
icon,
styles,
target,
onChange,
}: MyButtonProps) {
return (
<Link
target={target}
type="submit"
onChange={onChange}
rel="noopener noreferrer"
to={path!}
onClick={onClick}
className={`${styles} bg-primary text-white px-6 py-3 rounded-md flex justify-center items-center gap-2 text-sm active:scale-[.98] active:duration-75 hover:scale-[1.01] ease-in transition-all`}
>
{title} {icon}
</Link>
);
}

export default HSButton;
72 changes: 72 additions & 0 deletions src/components/form/HSInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
interface MyInputProps {
id?: string;
name?: string;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
onBlurTextArea?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
values?: string | number;
style?: string;
label?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChangeTextArea?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
placeholder: string;
type?: string;
text?: string;
icon?: JSX.Element;
}

function HSInput({
id,
name,
onBlur,
onBlurTextArea,
values,
style,
label,
onChange,
onChangeTextArea,
placeholder,
type,
text,
icon,
}: MyInputProps) {
return (
<div className="flex flex-col gap-2 w-full group">
<label htmlFor={id} className="text-md font-medium">
{label}
</label>
{type === 'input' ? (
<div
className={`${style} relative bg-grayLight text-black duration-100 outline-none justify-between flex items-center gap-2 px-3 w-full rounded-md font-light group-hover:border-grayDark`}
>
{icon && <p>{icon}</p>}
<input
type={text}
value={values}
onBlur={onBlur}
id={id}
name={name}
onChange={onChange}
placeholder={placeholder}
className="w-full h-full bg-transparent py-3 outline-none"
/>
</div>
) : (
<textarea
id={id}
name={name}
onBlur={onBlurTextArea}
cols={30}
rows={10}
placeholder={placeholder}
onChange={onChangeTextArea}
value={values}
className="text-black text-xs md:text-sm duration-150 w-full outline-none rounded-md border-[1px] group-hover:border-grayDark px-5 py-3"
>
{values}
</textarea>
)}
</div>
);
}

export default HSInput;
Loading

0 comments on commit 4b4b241

Please sign in to comment.