React Testing Librbary & Jest part 1
===

---
###### tags: `React Testing Library`, `Jest`, `React`
- Notes are from course [React Testing Library & Jest](https://www.udemy.com/course/react-testing-library-and-jest/) created by Stephen GRinder.
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
- [Jest](https://jestjs.io/)
## Section 1: Warm up!
- [Starter project](https://codesandbox.io/s/rtl-starter-sq54b4), please fork the project.
- Core functionality of this small project
1. App Starts up.
2. 6 products loaded.
3. Click load more.
4. 12 products loaded.
- Based on the core functionality, we can divid as 2 testing :
- First to test if there're 6 products loaded when app starts.
- Second to test if there're 12 products loaded when clicking 'Load More' btn.
```javascript=
// App.test.js
import { render, screen, waitFor } from '@testing-library/react'
import user from '@testing-library/user-event'
import App from './App'
test('show 6 products by default', async () => {
render(<App/>)
// Find headings
const headings = await screen.findAllByRole('heading')
expect(headings).toHaveLength(6)
})
test('Loaded more products when clicking the button', async () => {
render(<App/>)
const button = await screen.findAllByRole('button', {name: /load more/i})
// simulate click event
await user.click(button)
// It will take minutes to load 12 products
await waitFor(async () => {
const headings = await screen.findAllByRole('heading')
expect(headings).toHaveLength(12)
})
})
```
An error occured when running the test.

The issue was caused by using wrong syntax of `findAllByRole` in the second test, essentially there's only one button, therefore I should use `findByRole`
### Libraries vs purpose
| Library | Purpose |
| ---------------------------| -------------------------------------------------------|
| @testing-library/react | Uses ReactDOM to render a component for tesing |
| @testing-library/user-event| Helps simulate user input typing and clicking |
| @testing-library/dom | Helps find elements that are rendered by our components|
| jest | Runs our tests, reposrts results |
| jsdom | Simulates a browser when runing in a Node enviornment |
### How to find test file?
Jest finds all files in the src folder that have:
1. End with `.spec.js/ts`
2. End with `.test.js/ts`
3. There are placed in a folder called `__test__`
## Start testing
Let's create a simple form.
```javascript
// UserForm.jsx
import { useState } from 'react';
function UserForm({ onUserAdd }) {
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
onUserAdd({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<input value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div>
<label>Email</label>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<button>Add User</button>
</form>
);
}
export default UserForm;
```
## Test cases:
### Test if there's 2 input fields and 1 button.

```javascript
// UserForm.test.js
import { render, screen } from '@testing-library/react';
import UserForm from './UserForm';
test('It shows two inputs and a button', () => {
render(<UserForm />);
const inputs = screen.getAllByRole('textbox');
const button = screen,getByRole('button');
expect(inputs).toHaveLength(2);
expect(button).toBeInTheDocument();
})
```

#### Dive deep in ByRole
Here are APIs for roles: RTL find elements **based upon role**.
- getByRole
- queryByRole
- getAllByRole
- queryAllByRole
- findByRole
- findAllByRole
#### Rule of thumb:
1. Queries for elements with the given role (and it also accepts a TextMatch).
2. Default roles are taken into consideration without explicitly setting the role attribute.
3. Setting a role and/or aria-* attribute that matches the implicit ARIA semantics is unnecessary and is not recommended as these properties are already set by the browser.
4. Must not use the role and aria-* attributes in a manner that conflicts with the semantics described.
> [Use of ARIA attributes in HTML](https://www.w3.org/TR/html-aria/#docconformance)
#### Matchers
```javascript
expect(inputs).toHaveLength(2);
expect(button).toBeInTheDocument();
```
Here the `toHaveLength()` and `toBeInTheDocument()` are matchers from Jest.
>[Matchers](https://jestjs.io/docs/using-matchers)
### Test `onUserAdd` gets name and email

Here're two implementations, the first one is to show not being the best implementation.
```javascript
// UserForm.test.js
// NOT THE BEST IMPLEMENTATION
// Let's write down what we are going to do
test('Calls onUserAdd when the form is submitted', () =>{
// render component
render(<UserForm/>)
// Find inputs
// We don't need to find all of them, instead, we need to grab each of them
const [nameInput, emailInput] = screen.findAllByRole('textbox')
// simulate clicking name input
user.click(nameInput)
// simulate typing a name
user.keyboar('John')
// simulate clicking email input
user.click(emailInput)
// simulate typing an email
user.keyboard('john@test.com')
// find button
const button = screen.findByRole('button');
// simulate clicking button
user.click(button)
})
```
Let's run test to see the result, open terminal and run `npm run test`.
You might see there're quite a lot of messages show up due to `npm run test` is going to test all of the test files, but what if I just want to test one file ?

Typing `p` to use Pattern mode, it allows you to enter the test file name, here I entered `UserFo`, it would automatically show the search result.

Use arrow keyboard to select the file you want to test. Now the result said that the one of the test has failed, let's see the error message.

Why `onUserAdd` is not a function?
Because when we render `<UserForm/>` component, it simply rendered the component itself without props, but in our code, `onUserAdd` is a props.
Here we can create a list to store the value we've written in the test, and create another function as when it calls, it will push parameters to the list.
We can expect :
1. The length of list should be 1 (if test fails, length should be zero)
2. Inside the list should have an object of name and email with the value.
```javascript
test('calls onUserAdd when the form is submitted', () => {
const argList = [];
const callback = (...args) => {
argList.push(args);
};
// method 1: Not the best implementation
// render component
render(<UserForm onUserAdd={callback} />);
// find 2 inputs
const [nameInput, emailInput] = screen.getAllByRole('textbox');
// simulate clicking name input
user.click(nameInput);
// simulate typing a name
user.keyboard('Andrea');
// simulate click email input
user.click(emailInput);
// simulate typing in an email
user.keyboard('andrea@gmail.com');
// find button
const button = screen.getByRole('button');
// simulate clicking button
user.click(button);
// assertion to make sue onUserAdd gets called with email and name
expect(argList).toHaveLength(1);
expect(argList[0][0]).toEqual({ name: 'Andrea', email: 'andrea@gmail.com' });
});
```

---
Here we are going to amend test code above.
Codes that we need to fix:
- `callback` function: Here we can use `mock function` and test the mock function to see if the results are what we expected.
```javascript
test('calls onUserAdd when the form is submitted', () => {
const mock = jest.fn();
// render component
render(<UserForm onUserAdd={mock}/>);
// find 2 inputs
const [nameInput, emailInput] = screen.getAllByRole('textbox');
// simulate clicking name input
user.click(nameInput);
// simulate typing a name
user.keyboard('Andrea');
// simulate click email input
user.click(emailInput);
// simulate typing in an email
user.keyboard('andrea@gmail.com');
// find button
const button = screen.getByRole('button');
// simulate clicking button
user.click(button);
// assertion to make sue onUserAdd gets called with email and name
expect(mock).toHaveBeenCalled();
expect(mock).toBeCalledWith({ name: 'Andrea', email: 'andrea@gmail.com' });
});
```
We replaced the actual function to mock function, ideally, we can expect the mock function has been called and along with the object of name and email adress.

- Finds inputs: We have destructure as two inputs which were `nameInput` and `emailInput` in order, but what if the order of these elements change in the future? Ideally, we need to make sure we don't find elements just by its role, but also by finding the labels.
```javascript
test('calls onUserAdd when the form is submitted', () => {
const mock = jest.fn();
// method 1: Not the best implementation
// render component
render(<UserForm onUserAdd={mock} />);
// find 2 inputs
const nameInput = screen.getByRole('textbox', { name: /name/i });
const emailInput = screen.getByRole('textbox', { name: /email/i });
// simulate clicking name input
user.click(nameInput);
// simulate typing a name
user.keyboard('Andrea');
// simulate click email input
user.click(emailInput);
// simulate typing in an email
user.keyboard('andrea@gmail.com');
// find button
const button = screen.getByRole('button');
// simulate clicking button
user.click(button);
// assertion to make sue onUserAdd gets called with email and name
expect(mock).toHaveBeenCalled();
expect(mock).toBeCalledWith({ name: 'Andrea', email: 'andrea@gmail.com' });
});
```
> `/tet/i`, `i` means to ignore case.
> [TextMatch Example](https://testing-library.com/docs/queries/about/#textmatch-examples)
> [User Actions](https://testing-library.com/docs/dom-testing-library/api-events)
> [Mock Function](https://jestjs.io/docs/mock-function-api/)
---
### Test user list:Render one row per user

Sometimes we are not sure which method should we use to find element, we can use `screen.logTestingPlaygroundURL()` to check recommendation visually.
```javascript
// UserList.test.js
import { render, screen } from '@testing-library/react'
import UserList from './UserList'
test('render one row per user', () => {
const users = [
{ name: 'Andy', email: 'andy@test.com' },
{ name: 'Frank', email: 'frank@test.com' },
];
// <UserList /> has users props, so we need to pass a props
render(<UserList users={users} />);
// bring out testing playground
screen.logTestingPlaygroundURL();
})
```
Run test, you will see this url, `ctrl` / `command` and click the link to open in the browser.

You can hover the elements to get recommendations.

This playground has its downside, for example, we can't select a row entirely, it only allows you to select name or email seperately, in this case, we can add styles to the row as a workaround, see screen below.

Let's use `row` as the suggstion from the playground to see if we get the result correc.
Below to create a users list due to `<UsersList/>` has a props `users`, and we want to get rows and expect to have 2 row becuase we have 2 objects in user list.
```javascript
import { render, screen } from '@testing-library/react';
import UserList from './UserList';
test('render one row per user', () => {
const users = [
{ name: 'Andy', email: 'andy@test.com' },
{ name: 'Frank', email: 'frank@test.com' },
];
render(<UserList users={users} />);
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(2);
});
```
Turns out the result was failed, we've got rows in total, looked the error message, the result was includ the table headers (Name and Email), how can we fix it?

You might think we can try to get `tbody`, but the diarams shows that whether `tbody` or `thead`, they both have same role `rowgroup`, and it's not realistic to get the right `query` one by one.

#### TWO ESCAPE HATCHES
Ways to find elements when the preferred `role` approach doesn't work.
1. **data-testid**
2. **container.querySelector()**
##### **Using data-testid**
Inside the component, adding `data-testid`.
```javascript
// UserList.js
const UserList = ({users}) => {
...
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody data-testid="users">{renderedUsers}</tbody>
</table>
);
}
```
```javascript
// UserList.test.js
import { render, screen, within } from '@testing-library/react';
import UserList from './UserList';
test('render one row per user', () => {
const users = [
{ name: 'Andy', email: 'andy@test.com' },
{ name: 'Frank', email: 'frank@test.com' },
];
render(<UserList users={users} />);
const rows = within(screen.getByTestId('users')).getAllByRole('row');
expect(rows).toHaveLength(2);
});
```

>💡 Although we pass the test, but this approach made us to add additional syntax to our component only because of testing.
> A better way of deciding if we should implement this approach is to ask team or lead to make sure it's okay.
---
##### **Using container.querySelector()**
By default, React Testing Library will create a `div` and append that div to the document.body and this is where your React component will be rendered.

We can use this `div` and implement `querySelector` to query `tbody` and `tr`.
```javascript
import { render, screen, within } from '@testing-library/react';
import UserList from './UserList';
test('render one row per user', () => {
const users = [
{ name: 'Andy', email: 'andy@test.com' },
{ name: 'Frank', email: 'frank@test.com' },
];
const { container } = render(<UserList users={users} />);
const rows = container.querySelectorAll('tbody tr');
expect(rows).toHaveLength(2);
});
```
You will see there's two warnings here:


But the approach here is totally valid, so we can add a comment ` // eslint-disable-next-line` on top of it to remove the warning.
### Test user list: Render name and email of each user
This time we are going to check if the component renders email and name of each user, let's use the playground to see which query should we use.
The playground suggests that we can use `getByRole('cell', {name: /andy/i})`.

The suggestion saying that we can grab the role of **cell** with specify the **name**, we can use for loop to loop user list, and target **cell** and the name and email.
```javascript
test('render the email and name of each user', () => {
const users = [
{ name: 'Andy', email: 'andy@test.com' },
{ name: 'Frank', email: 'frank@test.com' },
];
render(<UserList users={users} />);
// screen.logTestingPlaygroundURL();
for (let user of users) {
const name = screen.getByRole('cell', { name: user.name });
const email = screen.getByRole('cell', { name: user.email });
expect(name).toBeInTheDocument();
expect(email).toBeInTheDocument();
}
});
```
## Clean up duplicated code.
We can see there're some duplications inside our test code, in these two tests, they all need a user list and render the `<UserList/>` component, we can move them into a helper function to return the user list.
```javascript!
const users = [
{ name: 'Andy', email: 'andy@test.com' },
{ name: 'Frank', email: 'frank@test.com' },
];
render(<UserList users={users} />);
```
```javascript!
import { render, screen, within } from '@testing-library/react';
import UserList from './UserList';
const renderComponent = () => {
const users = [
{ name: 'Andy', email: 'andy@test.com' },
{ name: 'Frank', email: 'frank@test.com' },
];
render(<UserList users={users} />);
return { users };
};
test('render one row per user', () => {
renderComponent();
const rows = within(screen.getByTestId('users')).getAllByRole('row');
expect(rows).toHaveLength(2);
});
test('render the email and name of each user', () => {
const { users } = renderComponent();
// screen.logTestingPlaygroundURL();
for (let user of users) {
const name = screen.getByRole('cell', { name: user.name });
const email = screen.getByRole('cell', { name: user.email });
expect(name).toBeInTheDocument();
expect(email).toBeInTheDocument();
}
});
```
### Test App
Now let's test the whole App, in this test, we will test everything we've done before in our component test, to make sure the app can render name and email in the document.
```javascript!
// App.test.js
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import App from './App';
test('Can receive a new user and show on a list', () => {
// render component
render(<App />);
// Grab name and email inputs and button
const nameInput = screen.getByRole('textbox', { name: /name/i });
const emailInput = screen.getByRole('textbox', { name: /email/i });
const button = screen.getByRole('button');
// Simulate click input field and typing
user.click(nameInput);
user.keyboard('Andy');
user.click(emailInput);
user.keyboard('andy@test.com');
// Simulate submitting
user.click(button);
// Using debug() to see result in the terminal
screen.debug()
})
```
We can see that the name and email are in the document successfully, now we can remove `screen.debug()` and grab result from table and write assertions.

```javascript!
test('Can receive a new user and show it on a list', () => {
// render component
render(<App />);
// Find 2 inputs and simulate typing
const nameInput = screen.getByRole('textbox', { name: /name/i });
const emailInput = screen.getByRole('textbox', { name: /email/i });
const button = screen.getByRole('button');
user.click(nameInput);
user.keyboard('Andy');
user.click(emailInput);
user.keyboard('andy@test.com');
user.click(button);
// Grab result from document
const name = screen.getByRole('cell', { name: 'Andy' });
const email = screen.getByRole('cell', { name: 'andy@test.com' });
// assertion
expect(name).toBeInTheDocument();
expect(email).toBeInTheDocument();
});
```
