This project is part of the Test Driven Development (TDD) en React JS course.
This form has been developed applying Test Driven Development with , , , and
Project URL is available on GitHub Pages.
NOTE: a mock server has been used to develop this exercise.
Instructions to start this project:
- Clone repository:
git clone [repository]
- Install NPM packages and dependencies:
npm install
- Run project on local server
npm start
- Run tests:
npm run test
Login and Authentication
As company app user, I want a login page as a way of have a protected access to the app.
Acceptance Criteria
-
There must be a login page.
-
The login page must have a form with the following fields: email, password and a submit button.
describe('when login page is mounted', () => {
it('must display the login title', () => {
expect(screen.getByText(/login page/i)).toBeInTheDocument()
})
it('must have a form with the following fields: email, password and a submit button', () => {
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.getByLabelText(/password/i)).toBeInTheDocument()
expect(screen.getByRole('button', {name: /send/i}))
})
})
-
The email and password inputs are required.
-
If the user leaves empty fields and clicks the submit button, the login page should display required messages as the format: “The [field name] is required” aside of the proper field.
describe('when the user leaves empty fields and clicks the submit button', () => {
it('display required messages as the format: “The [field name] is required”', async () => {
expect(screen.queryByText(/the email is required/i)).not.toBeInTheDocument()
expect(
screen.queryByText(/the password is required/i),
).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', {name: /send/i}))
expect(screen.getByText(/the email is required/i)).toBeInTheDocument()
expect(screen.getByText(/the password is required/i)).toBeInTheDocument()
await waitFor(() => expect(getSendButton()).not.toBeDisabled())
})
})
- The email and password inputs are validated.
describe('when the user fills the fields and clicks the submit button', () => {
it('must not display the required messages', async () => {
fillInputs()
fireEvent.click(screen.getByRole('button', {name: /send/i}))
expect(screen.queryByText(/the email is required/i)).not.toBeInTheDocument()
expect(
screen.queryByText(/the password is required/i),
).not.toBeInTheDocument()
await waitFor(() => expect(getSendButton()).not.toBeDisabled())
})
})
- The email value should contain the proper email format (the “@”, domain value, etc).
describe('when the user fills and blur the email input with invalid email, and then focus and change with valid value', () => {
it('must not display a validation message', () => {
const emailInput = screen.getByLabelText(/email/i)
fireEvent.change(emailInput, {
target: {value: 'invalid.email'},
})
fireEvent.blur(emailInput)
expect(
screen.getByText(/the email is invalid. example: [email protected]/i),
).toBeInTheDocument()
fireEvent.change(emailInput, {
target: {value: '[email protected]'},
})
fireEvent.blur(emailInput)
expect(
screen.queryByText(/the email is invalid. example: [email protected]/i),
).not.toBeInTheDocument()
})
})
- The password input should contain at least: 8 characters, one upper case letter, one number and one special character.
describe('when the user fills and blur the password input with a value with 7 character length', () => {
it(`must display the validation message "The password must contain at least 8 characters,
one upper case letter, one number and one special character"`, () => {
const passwordSevenLengthVal = 'asdfghj'
fireEvent.change(getPasswordInput(), {
target: {value: passwordSevenLengthVal},
})
fireEvent.blur(getPasswordInput())
expect(screen.getByText(passwordValidationMessage)).toBeInTheDocument()
})
})
describe('when the user fills and blur the password input with a value without one upper case character', () => {
it(`must display the validation message "The password must contain at least 8 characters,
one upper case letter, one number and one special character"`, () => {
const passwordWithoutUpperCaseVal = 'asdfghj8'
fireEvent.change(getPasswordInput(), {
target: {value: passwordWithoutUpperCaseVal},
})
fireEvent.blur(getPasswordInput())
expect(screen.getByText(passwordValidationMessage)).toBeInTheDocument()
})
})
describe('when the user fills and blur the password input with a value without one number', () => {
it(`must display the validation message "The password must contain at least 8 characters,
one upper case letter, one number and one special character"`, () => {
const passwordWithoutNumb = 'asdfghjA'
fireEvent.change(getPasswordInput(), {
target: {value: passwordWithoutNumb},
})
fireEvent.blur(getPasswordInput())
expect(screen.getByText(passwordValidationMessage)).toBeInTheDocument()
})
})
describe(`when the user fills and blur the password input with without one special character and
then change with valid value and blur again`, () => {
it(`must not display the validation message`, () => {
const passwordWithoutSpecialChar = 'asdfghjA1a'
const validPassword = 'aA1asdasda#'
fireEvent.change(getPasswordInput(), {
target: {value: passwordWithoutSpecialChar},
})
fireEvent.blur(getPasswordInput())
expect(screen.getByText(passwordValidationMessage)).toBeInTheDocument()
fireEvent.change(getPasswordInput(), {
target: {value: validPassword},
})
fireEvent.blur(getPasswordInput())
expect(
screen.queryByText(passwordValidationMessage),
).not.toBeInTheDocument()
})
})
-
The password input should contain at least: 8 characters, one upper case letter, one number and one special character.
-
The form must send the data to a backend endpoint service.
-
The submit button should be disabbled while the form page is fetching the data. After fetching, the submit button does not have to be disabled.
-
There should be a loading indicator at the top of the form while it is fetching.
describe('when the user submit the login form with valid data', () => {
it('must disable the submit button while the form page is fetching the data', async () => {
fillInputs()
fireEvent.click(getSendButton())
expect(getSendButton()).toBeDisabled()
await waitFor(() => expect(getSendButton()).not.toBeDisabled())
})
it('must be a loading indicator at the top of the form while it is fetching', async () => {
fillInputs()
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument()
fireEvent.click(getSendButton())
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument()
await waitForElementToBeRemoved(() =>
screen.queryByTestId('loading-indicator'),
)
})
})
-
In a unexpected server error, the form page must display the error message “Unexpected error, please try again” from the api.
-
In the invalid credentials response, the form page must display the error message “The email or password are not correct” from the api.
-
Not authenticated users must be redirected to the login page on enter to private pages (employees and admin pages).
describe('when the user submit the login form with valid data and there is an unexpected server error', () => {
it('must display the error message "Unexpected error, please try again" from the api', async () => {
server.use(
rest.post('/login', (req, res, ctx) =>
res(
ctx.status(HTTP_UNEXPECTED_ERROR_STATUS),
ctx.json({message: 'Unexpected error, please try again'}),
),
),
)
expect(
screen.queryByText(/unexpected error, please try again/i),
).not.toBeInTheDocument()
fillInputs()
fireEvent.click(getSendButton())
expect(
await screen.findByText(/unexpected error, please try again/i),
).toBeInTheDocument()
})
})
describe('when the user submit the login form with valid data and there is an invalid credentials error', () => {
it('must display the error message "The email or password are not correct" from the api', async () => {
const wrongEmail = '[email protected]'
const wrongPassword = 'Aa12345678$'
server.use(handlerInvalidCredentials({wrongEmail, wrongPassword}))
expect(
screen.queryByText(/the email or password are not correct/i),
).not.toBeInTheDocument()
fillInputs({email: wrongEmail, password: wrongPassword})
fireEvent.click(getSendButton())
expect(
await screen.findByText(/the email or password are not correct/i),
).toBeInTheDocument()
})
})
Authorization
As admin, I want to have full access to the company app modules: employees page and admin page as a way of operate them.
Acceptance Criteria
- The user must be redirected to the login page when is not authenticated and enters on the admin page or on the employee page.
describe('when the user is not authenticated and enters on admin page', () => {
it('must be redirected to login page', () => {
goTo('/admin')
renderWithAuthProvider(<AppRouter />)
expect(screen.getByText(/login page/i)).toBeInTheDocument()
})
})
describe('when the user is not authenticated and enters on employee page', () => {
it('must be redirected to login page', () => {
goTo('/employee')
renderWithAuthProvider(<AppRouter />)
expect(screen.getByText(/login page/i)).toBeInTheDocument()
})
})
- The admin must be redirected to the admin page after login.
describe('when the admin is authenticated in login page', () => {
it('must be redirected to admin page', async () => {
renderWithAuthProvider(<AppRouter isAuth />)
fillInputs({email: ADMIN_EMAIL})
fireEvent.click(getSendButton())
expect(await screen.findByText(/admin page/i)).toBeInTheDocument()
expect(await screen.findByText(/john doe/i)).toBeInTheDocument()
})
})
- The admin username should be displayed on the common navbar.
describe('when the admin page is mounted', () => {
it('must display the admin username', () => {
renderWithAuthProvider(
<AuthContext.Provider value={{user: {username: 'John Doe'}}}>
<AdminPage />
</AuthContext.Provider>,
)
expect(screen.getByText(/john doe/i)).toBeInTheDocument()
})
})
- The admin must have access to the employees page.
describe('when the admin goes to employees page', () => {
it('must have access', () => {
goTo('/admin')
renderWithAuthProvider(<AppRouter />, {isAuth: true, role: ADMIN_ROLE})
fireEvent.click(screen.getByText(/employee/i))
expect(screen.getByText(/employee page/i)).toBeInTheDocument()
})
})
- The admin must have access to delete the employee button.
describe('when the admin access to employee page', () => {
it('must have access to delete the employee button', () => {
renderWith({role: ADMIN_ROLE})
expect(screen.getByRole('button', {name: /delete/i})).toBeInTheDocument()
})
})
Authorization
As an employee, I want to have access to the employees page.
Acceptance Criteria
- The employee must be redirected to the employees page after login and have access to employees page
describe('when the employee is authenticated in login page', () => {
it('must be redirected to employee page', async () => {
renderWithAuthProvider(<AppRouter isAuth />)
fillInputs({email: EMPLOYEE_EMAIL})
fireEvent.click(getSendButton())
expect(await screen.findByText(/employee page/i)).toBeInTheDocument()
})
})
-
The employee must not have access to delete the employee button.
-
The employee username should be displayed on the common navbar.
describe('when the employee access to employee page', () => {
it('must not have access to delete the employee button', () => {
renderWith({role: EMPLOYEE_ROLE})
expect(
screen.queryByRole('button', {name: /delete/i}),
).not.toBeInTheDocument()
})
it('the employee username should be displayed on the common navbar', () => {
renderWith({role: EMPLOYEE_ROLE, username: 'Joana Doe'})
expect(expect(screen.getByText(/joana doe/i)).toBeInTheDocument())
})
})
-
The employee must not have access to the admin page.
-
The forbidden page access must be redirected to the allowed page.
describe('when the employee goes to admin page', () => {
it('must redirect to employee page', () => {
goTo('/admin')
renderWithAuthProvider(<AppRouter />, {isAuth: true, role: EMPLOYEE_ROLE})
expect(screen.getByText(/employee page/i)).toBeInTheDocument()
})
})
React TDD Login Form
├── docs
├── node_modules
├── public
├── src
│ ├── admin/components/admin-page
│ │ ├── admin-page.js
│ │ ├── admin-page.test.js
│ │ └── index.js
│ ├── auth
│ │ ├── components/login-page
│ │ │ ├── index.js
│ │ │ ├── login-page.js
│ │ │ └── login-page.test.js
│ │ └── services
│ │ └── index.js
│ ├── const
│ │ └── index.js
│ ├── employee/components/employee-page
│ │ ├── employee-page.js
│ │ ├── employee-page.test.js
│ │ └── index.js
│ ├── images
│ │ └── form.png
│ ├── mocks
│ │ ├── browser.js
│ │ └── handlers.js
│ ├── utils
│ │ ├── components
│ │ │ ├── auth-guard.js
│ │ │ ├── private-route.js
│ │ │ └── user-layout.js
│ │ ├── contexts
│ │ │ └── auth-context.js
│ │ ├── helpers.js
│ │ └── tests.js
│ ├── app-router.js
│ ├── app-router.test.js
│ ├── App.js
│ ├── index.js
│ └── setupTests.js
|── .eslintrc
├── .gitignore
├── .prettierrc
├── LICENSE
├── package-lock.json
├── package.json
└── README.md