Skip to content

Commit

Permalink
Merge pull request #22 from SAINIAbhishek/react_testing
Browse files Browse the repository at this point in the history
testing setup and basic integration
  • Loading branch information
SAINIAbhishek authored Aug 12, 2024
2 parents 6d9f8c8 + dc3b3f5 commit 769bc77
Show file tree
Hide file tree
Showing 20 changed files with 507 additions and 50 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ The purpose of this project is to demonstrate a fully functional FullStack Task
![Home page](home.png)

## Frontend (React + TypeScript + Vite + Tailwind CSS)
The project's organization aligns with established industry standards, employing a feature-based directory structure and maintaining a uniform naming convention.

The project's organization aligns with established industry standards, employing a feature-based directory structure and maintaining a uniform naming convention.

Additionally, the application employs the Context API and Hooks to effectively manage its state.

## Stacks:

- TypeScript
- Formik
- React Query
Expand All @@ -25,6 +27,7 @@ Additionally, the application employs the Context API and Hooks to effectively m
- Eslint
- Prettier
- React i18next
- Testing with Vitest & React Testing Library

<hr>

Expand Down Expand Up @@ -58,6 +61,7 @@ The project structure follows the best practices and conventions of a Node.js ap
The project has directories based on the functionality and type while justifying the directory name.

Following are the API features of this project:

- **TypeScript:** This backend is written in TypeScript, enhancing the development experience by adding static typing to JavaScript. This results in more reliable and maintainable codebases, helps catch potential errors during development, and provides better code completion and tooling support.
- **Request Limiter**: This feature prevents abuse or overload of the login route, forgot password route by limiting the number of requests, enhancing security and preventing potential attacks.
- **Centralized Error Handling:** Centralized error handling and response management streamline the codebase, making it easier to maintain and ensuring a consistent user experience.
Expand All @@ -73,6 +77,7 @@ Following are the API features of this project:
- **Middleware for Exception Handling:** Middleware for handling exceptions within async Express routes and forwarding them to Express error handlers improves error management, ensuring the smooth operation of the application.

## API Stacks:

- Node.js
- Express.js
- Typescript
Expand All @@ -87,6 +92,7 @@ Following are the API features of this project:
- Nodemailer

## Setup MAILTRAP

To test the email functionality I've used the Mailtrap service: <a href="https://mailtrap.io/" target="_blank">Mailtrap</a>.
You can also create your credentials and place them in the .env file under the **Mailtrap(Email service) Info**.

Expand Down Expand Up @@ -121,6 +127,7 @@ To generate a secret token and refresh to add in the .env file of the server you
```
node
```

```
require('crypto').randomBytes(64).toString('hex')
```
2 changes: 2 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ package-lock.json
.env
.env.production

coverage/*

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand Down
29 changes: 22 additions & 7 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "npm run lint --fix",
"preview": "vite preview",
"test": "vitest",
"test:clearCache": "vitest --clearCache",
"test:ui": "vitest --ui",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage",
"prettier": "prettier . --write",
"install:packages": "npm i",
"upgrade:packages": "npm update --save-dev && npm update --save",
Expand All @@ -18,7 +23,7 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/react-query": "^5.51.21",
"@tanstack/react-query": "^5.51.23",
"axios": "^1.7.3",
"date-fns": "^2.30.0",
"dotenv": "^16.4.5",
Expand All @@ -29,7 +34,7 @@
"react-datepicker": "^7.3.0",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.0",
"react-i18next": "^15.0.1",
"react-router-dom": "^6.26.0",
"yup": "^1.4.0"
},
Expand All @@ -46,15 +51,23 @@
]
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.14",
"@tanstack/eslint-plugin-query": "^5.51.15",
"@tanstack/react-query-devtools": "^5.51.21",
"@types/node": "^16.18.104",
"@tanstack/react-query-devtools": "^5.51.23",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/node": "^18.0.0",
"@types/react": "^18.3.3",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^18.3.0",
"@types/testing-library__jest-dom": "^5.14.9",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.3.1",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/ui": "^2.0.5",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
Expand All @@ -64,10 +77,12 @@
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.9",
"postcss": "^8.4.40",
"jsdom": "^24.1.1",
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.7",
"tailwindcss": "^3.4.8",
"typescript": "^5.5.4",
"vite": "^4.5.3"
"vite": "^4.5.3",
"vitest": "^2.0.5"
}
}
74 changes: 74 additions & 0 deletions frontend/src/components/buttons/icon-btn/__tests__/indes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { vi } from 'vitest';
import IconButton, { IconButtonProps } from '..';

describe('IconButton', () => {
const renderButton = (props: IconButtonProps) => {
render(<IconButton {...props} />);
};

test('renders the button with children', () => {
renderButton({ children: <span>Icon</span> });
expect(screen.getByText('Icon')).toBeInTheDocument();
});

test('renders with disabled state', () => {
renderButton({ children: <span>Icon</span>, isDisabled: true });
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(button).toHaveClass('cursor-not-allowed');
});

test('renders with loading state', () => {
renderButton({ children: <span>Icon</span>, isLoading: true });
const button = screen.getByRole('button');
expect(button).toHaveClass('cursor-not-allowed');
});

test('calls handleClick when clicked', () => {
const handleClick = vi.fn();
renderButton({ children: <span>Icon</span>, handleClick });
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

test('does not call handleClick when disabled', () => {
const handleClick = vi.fn();
renderButton({
children: <span>Icon</span>,
handleClick,
isDisabled: true,
});
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});

test('applies the submit type', () => {
renderButton({ children: <span>Icon</span>, type: 'submit' });
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
});

test('applies the reset type', () => {
renderButton({ children: <span>Icon</span>, type: 'reset' });
expect(screen.getByRole('button')).toHaveAttribute('type', 'reset');
});

test('applies additional classes from className prop', () => {
renderButton({
children: <span>Icon</span>,
className: 'extra-class',
});
expect(screen.getByRole('button')).toHaveClass('extra-class');
});

test('translates the title', () => {
renderButton({
children: <span>Icon</span>,
title: 'translated.title',
});
expect(screen.getByRole('button')).toHaveAttribute(
'title',
'translated.title',
);
});
});
4 changes: 2 additions & 2 deletions frontend/src/components/buttons/icon-btn/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

type Props = {
export type IconButtonProps = {
title?: string;
className?: string;
isDisabled?: boolean;
Expand All @@ -19,7 +19,7 @@ const IconButton = ({
children,
type = 'button',
title,
}: Props) => {
}: IconButtonProps) => {
const { t } = useTranslation();

return (
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/components/buttons/link-btn/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { fireEvent, render, screen } from '@testing-library/react';
import LinkButton, { LinkButtonProps } from '..';
import { vi } from 'vitest';

describe('LinkButton', () => {
const title = 'Title';

const renderButton = (props: LinkButtonProps) => {
render(<LinkButton {...props} />);
};

test('renders the button with title', () => {
renderButton({ title: title });
expect(screen.getByText(title)).toBeInTheDocument();
});

test('renders with disabled state', () => {
renderButton({ title: title, isDisabled: true });
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});

test('calls handleClick when clicked', () => {
const handleClick = vi.fn();
renderButton({ title: title, handleClick });
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

test('does not call handleClick when disabled', () => {
const handleClick = vi.fn();
renderButton({ title: title, handleClick, isDisabled: true });
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});

test('applies the reset type', () => {
renderButton({ title: title, type: 'reset' });
expect(screen.getByRole('button')).toHaveAttribute('type', 'reset');
});

test('applies the submit type', () => {
renderButton({ title: title, type: 'submit' });
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
});

test('applies additional classes from className prop', () => {
renderButton({ title: title, className: 'extra-class' });
expect(screen.getByRole('button')).toHaveClass('extra-class');
});

test('translates the title', () => {
renderButton({ title: 'translated.title' });
expect(screen.getByText('translated.title')).toBeInTheDocument();
});
});
4 changes: 2 additions & 2 deletions frontend/src/components/buttons/link-btn/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';

type Props = {
export type LinkButtonProps = {
title: string;
className?: string;
isDisabled?: boolean;
Expand All @@ -14,7 +14,7 @@ const LinkButton = ({
handleClick,
isDisabled,
type = 'button',
}: Props) => {
}: LinkButtonProps) => {
const { t } = useTranslation();

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { vi } from 'vitest';
import PrimaryButton, { PrimaryButtonProps } from '..';

describe('PrimaryButton', () => {
const title = 'Title';

const renderPrimaryButton = (props: PrimaryButtonProps) => {
render(<PrimaryButton {...props} />);
};

test('renders the button with title', () => {
renderPrimaryButton({ title: title });
expect(screen.getByText(title)).toBeInTheDocument();
});

test('renders with loading state', () => {
renderPrimaryButton({ title: title, isLoading: true });
expect(screen.queryByText(title)).not.toBeInTheDocument();
expect(screen.getByTestId('spinner')).toBeInTheDocument();
expect(screen.getByTestId('spinner')).toHaveClass(
'h-6',
'w-6',
'border-t-2',
'border-white',
);
});

test('calls handleClick when clicked', () => {
const handleClick = vi.fn();
renderPrimaryButton({ title: title, handleClick });
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

test('calls handleClick when clicked', () => {
const handleClick = vi.fn();
renderPrimaryButton({ title: title, handleClick });
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

test('does not call handleClick when disabled', () => {
const handleClick = vi.fn();
renderPrimaryButton({ title: title, handleClick, isDisabled: true });
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});

test('applies the correct type', () => {
renderPrimaryButton({ title: title, type: 'submit' });
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
});

test('applies additional classes from className prop', () => {
renderPrimaryButton({ title: title, className: 'extra-class' });
expect(screen.getByRole('button')).toHaveClass('extra-class');
});

test('translates the title', () => {
renderPrimaryButton({ title: 'translated.title' });
expect(screen.getByText('translated.title')).toBeInTheDocument();
});
});
4 changes: 2 additions & 2 deletions frontend/src/components/buttons/primary-btn/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Spinner from '@/components/spinner';
import { useTranslation } from 'react-i18next';

type Props = {
export type PrimaryButtonProps = {
title: string;
className?: string;
isLoading?: boolean;
Expand All @@ -17,7 +17,7 @@ const PrimaryButton = ({
isLoading,
isDisabled,
type = 'button',
}: Props) => {
}: PrimaryButtonProps) => {
const { t } = useTranslation();

return (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/layout/content-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Props = {

const ContentLayout = ({ children, className }: Props) => {
return (
<div className={`max-w-screen-2xl mx-auto flex mx-8 px-4 ${className}`}>
<div className={`max-w-screen-2xl flex mx-8 px-4 ${className}`}>
{children}
</div>
);
Expand Down
Loading

0 comments on commit 769bc77

Please sign in to comment.