Skip to content

Commit

Permalink
setup envirnoment (#24)
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

resolve mismatch uri
  • Loading branch information
wayneleon1 authored Jun 27, 2024
1 parent 4c0c5b6 commit b9b7d7f
Show file tree
Hide file tree
Showing 15 changed files with 1,698 additions and 791 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',
],
};
2 changes: 2 additions & 0 deletions .github/workflows/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ jobs:
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
env:
VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
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
207 changes: 207 additions & 0 deletions src/__test__/signInSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
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: 'Logout Successfully',
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: '[email protected]' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);

await waitFor(() => {
expect(screen.getByTestId('Loading')).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');

fireEvent.change(emailInput, { target: { value: '[email protected]' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.submit(screen.getByTestId('form'));

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 b9b7d7f

Please sign in to comment.