Skip to content

Commit fb10b5e

Browse files
save progress
1 parent 5a63495 commit fb10b5e

30 files changed

+498
-67
lines changed

frontend/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"babel-jest": "^29.3.1",
5353
"canvas": "^2.11.0",
5454
"jest": "^27.5.1",
55+
"moment": "^2.29.4",
5556
"msw": "^0.49.3"
5657
},
5758
"jest": {

frontend/src/__tests__/App.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { rest } from 'msw';
2+
import { setupServer } from 'msw/node';
3+
import { fireEvent, waitFor } from '@testing-library/react';
4+
import renderWithProvider from 'utils/renderWithProvider';
5+
import App from 'app';
6+
import { addNotification } from 'store/notificationsSlice';
7+
import { act } from 'react-dom/test-utils';
8+
9+
describe('App', () => {
10+
const server = setupServer();
11+
12+
beforeAll(() => server.listen());
13+
beforeEach(() => {
14+
server.resetHandlers();
15+
// default handlers for /api/users and /api/countries
16+
server.use(
17+
rest.get('/api/users', (req, res, ctx) => res(ctx.json({ users: [], totalPages: 1 })))
18+
);
19+
server.use(
20+
rest.get('/api/countries', (req, res, ctx) => res(ctx.json([])))
21+
);
22+
});
23+
afterAll(() => server.close());
24+
25+
// axios.interceptors.response.use doesn't work in tests, so we can't test notifications for http errors
26+
if (false) {
27+
it('notifies user about http errors', async () => {
28+
server.use(
29+
rest.get('/api/users', (req, res, ctx) => res(ctx.status(500), ctx.json({ message: 'Network error' })))
30+
);
31+
const { getByText } = renderWithProvider(<App />);
32+
await waitFor(() => getByText('Network error'));
33+
});
34+
}
35+
36+
it('test notifications manually', async () => {
37+
const browser = renderWithProvider(<App />);
38+
await act(async () => {
39+
browser.store.dispatch(addNotification({
40+
title: 'Test notification',
41+
message: 'Test message',
42+
type: 'error',
43+
}));
44+
});
45+
46+
expect(browser.getByText('Test notification')).toBeInTheDocument();
47+
expect(browser.getByText('Test message')).toBeInTheDocument();
48+
});
49+
50+
it('should return to / page if cancelled creating a new user', async () => {
51+
const browser = renderWithProvider(<App />);
52+
// wait for the / page to load and click the 'New user' button
53+
await browser.findByText('New user');
54+
fireEvent.click(browser.getByText('New user'));
55+
// wait for the /add page to load and click the 'Cancel' button
56+
await browser.findByText('Save');
57+
expect(browser.getByText('Save')).toBeInTheDocument();
58+
expect(browser.store.getState().router.location.pathname).toBe('/add');
59+
// Cancel the creation of a new user
60+
fireEvent.click(browser.getByText('Cancel'));
61+
// 'Save' button is not present on the / page
62+
expect(browser.store.getState().router.location.pathname).toBe('/');
63+
expect(browser.queryByText('Save')).toBeNull();
64+
});
65+
66+
it('should create a new user', async () => {
67+
// mock the api
68+
let userCreated = false;
69+
server.use(
70+
rest.post('/api/users', (req, res, ctx) => {
71+
userCreated = true;
72+
return res(ctx.json({}));
73+
}),
74+
rest.get('/api/users', (req, res, ctx) => {
75+
return res(ctx.json({
76+
users: [{ id: 1, first_name: 'John', last_name: 'Doe', date_of_birth: '2000-01-02', country_name: 'USA', country_id: 1 }],
77+
totalPages: 1,
78+
}));
79+
})
80+
);
81+
///////////////////////////////
82+
const browser = renderWithProvider(<App />);
83+
// wait for the / page to load and click the 'New user' button
84+
await browser.findByText('New user');
85+
fireEvent.click(browser.getByText('New user'));
86+
// wait for the /add page to load and fill in the form
87+
await browser.findByText('Save');
88+
fireEvent.change(browser.getByLabelText('First Name'), { target: { value: 'John' } });
89+
fireEvent.change(browser.getByLabelText('Last Name'), { target: { value: 'Doe' } });
90+
fireEvent.change(browser.getByLabelText('Date of Birth'), { target: { value: '01-02-2000' } });
91+
fireEvent.change(browser.getByLabelText('Country'), { target: { value: 'new' } });
92+
fireEvent.change(browser.getByPlaceholderText('Enter new country name'), { target: { value: 'USA' } });
93+
// 'Save' button is enabled
94+
expect(browser.getByText('Save')).not.toBeDisabled();
95+
await act(async () => {
96+
fireEvent.click(browser.getByText('Save'));
97+
});
98+
expect(userCreated).toBe(true);
99+
100+
// wait for the / page to load and check that the new user is present
101+
await browser.findByText('John Doe');
102+
expect(browser.store.getState().router.location.pathname).toBe('/');
103+
expect(browser.getByText('John Doe')).toBeInTheDocument();
104+
});
105+
106+
it('should remove a user', async () => {
107+
// mock the api
108+
let userRemoved = false;
109+
server.use(
110+
rest.delete('/api/users/1', (req, res, ctx) => {
111+
userRemoved = true;
112+
return res(ctx.json({}));
113+
}),
114+
rest.get('/api/users', (req, res, ctx) => {
115+
if (userRemoved) {
116+
return res(ctx.json({
117+
users: [],
118+
totalPages: 1,
119+
}));
120+
}
121+
return res(ctx.json({
122+
users: [{ id: 1, first_name: 'John', last_name: 'Doe', date_of_birth: '2000-01-02', country_name: 'USA', country_id: 1 }],
123+
totalPages: 1,
124+
}));
125+
})
126+
);
127+
///////////////////////////////
128+
const browser = renderWithProvider(<App />);
129+
// wait for the / page to load and click the 'New user' button
130+
await browser.findByText('John Doe');
131+
// click the 'Remove' button
132+
fireEvent.click(browser.getByRole('button', { name: 'Remove' }));
133+
// Click the 'Yes' button in the confirmation dialog
134+
await act(async () => {
135+
fireEvent.click(browser.getByText('Yes'));
136+
});
137+
138+
// wait for the / page to load and check that the user is removed
139+
expect(userRemoved).toBe(true);
140+
await browser.findByText('No users found');
141+
});
142+
143+
false && it('should edit a user', async () => {
144+
// mock the api
145+
let userEdited = false;
146+
server.use(
147+
rest.put('/api/users/1', (req, res, ctx) => {
148+
userEdited = true;
149+
return res(ctx.json({}));
150+
}),
151+
rest.get('/api/users', (req, res, ctx) => {
152+
return res(ctx.json({
153+
users: [{
154+
id: 1, last_name: 'Doe', date_of_birth: '2000-01-02', country_name: 'USA', country_id: 1,
155+
first_name: userEdited ? 'Jane' : 'John',
156+
}],
157+
totalPages: 1,
158+
}));
159+
})
160+
);
161+
///////////////////////////////
162+
const browser = renderWithProvider(<App />);
163+
// wait for the / page to load
164+
await browser.findByText('John Doe');
165+
// double click the user
166+
fireEvent.doubleClick(browser.getByText('John Doe'));
167+
// wait for the /edit page to load and fill in the form
168+
await browser.findByText('Save');
169+
fireEvent.change(browser.getByLabelText('First Name'), { target: { value: 'Jane' } });
170+
// 'Save' button is enabled
171+
expect(browser.getByText('Save')).not.toBeDisabled();
172+
await act(async () => {
173+
fireEvent.click(browser.getByText('Save'));
174+
});
175+
expect(userEdited).toBe(true);
176+
177+
// wait for the / page to load and check that the user is edited
178+
await browser.findByText('Jane Doe');
179+
expect(browser.store.getState().router.location.pathname).toBe('/');
180+
expect(browser.getByText('Jane Doe')).toBeInTheDocument();
181+
});
182+
});

frontend/src/__tests__/components/UsersDataGrid.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,23 @@ describe('UsersDataGrid', () => {
8787
expect(removeUser).toHaveBeenCalledWith(user.id);
8888
});
8989

90-
it('should be add "New user" button that calls addUser', () => {
90+
it('should call only removeUser when clicking on the remove button', () => {
91+
const removeUser = jest.fn();
92+
const viewUser = jest.fn();
93+
const user = { id: 123, first_name: 'Test', last_name: 'User', country_name: 'USA', age: 25 };
94+
const props = {
95+
...DEFAULT_PROPS,
96+
removeUser,
97+
viewUser,
98+
users: { ...DEFAULT_PROPS.users, data: [...DEFAULT_PROPS.users.data, user] },
99+
};
100+
const { container } = render(<UsersDataGrid {...props} />);
101+
const button = container.querySelector('tr:nth-child(3) button');
102+
fireEvent.click(button);
103+
expect(viewUser).not.toHaveBeenCalled();
104+
});
105+
106+
it('should be "New user" button that calls addUser', () => {
91107
const addUser = jest.fn();
92108
const props = { ...DEFAULT_PROPS, addUser };
93109
const { getByText } = render(<UsersDataGrid {...props} />);

frontend/src/__tests__/components/modals/AddUserModal.js

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
import React from 'react';
22
import { render, fireEvent, waitFor } from '@testing-library/react';
33
import AddUserModal from 'components/modals/AddUserModal';
4+
import moment from 'moment';
45

56
describe('AddUserModal', () => {
67
const countries = [
78
{ id: 1, name: 'USA' },
89
{ id: 2, name: 'Canada' },
910
{ id: 3, name: 'Mexico' },
1011
];
11-
function fakeDatePicker({ onChange, ...props }) {
12-
function handleChange(event) {
13-
onChange(event.target.value);
14-
}
15-
return <input type="text" {...props} data-testid="dateOfBirth" onChange={handleChange} />;
16-
}
1712

1813
function renderAddUserModal(args = {}) {
19-
const browser = render(<AddUserModal countries={countries} datePickerComponent={fakeDatePicker} {...args} />);
14+
const browser = render(<AddUserModal countries={countries} {...args} />);
2015
const METHOD = {
2116
'text': { true: browser.getByText, false: browser.queryByText },
2217
'labelText': { true: browser.getByLabelText, false: browser.queryByLabelText },
@@ -30,7 +25,7 @@ describe('AddUserModal', () => {
3025
lastName: (raise = true) => METHOD['labelText'][raise]('Last Name'),
3126
country: (raise = true) => METHOD['labelText'][raise]('Country'),
3227
newCountry: (raise = true) => METHOD['placeholderText'][raise]('Enter new country name'),
33-
dateOfBirth: (raise = true) => METHOD['testId'][raise]('dateOfBirth'),
28+
dateOfBirth: (raise = true) => METHOD['labelText'][raise]('Date of Birth'),
3429
saveButton: (raise = true) => METHOD['text'][raise]('Save'),
3530
cancelButton: (raise = true) => METHOD['text'][raise]('Cancel'),
3631
}
@@ -76,7 +71,7 @@ describe('AddUserModal', () => {
7671
fireEvent.change(UI.firstName(), { target: { value: 'John' } });
7772
fireEvent.change(UI.lastName(), { target: { value: 'Smith' } });
7873
fireEvent.change(UI.country(), { target: { value: 'Canada' } });
79-
fireEvent.change(UI.dateOfBirth(), { target: { value: '2019-10-10' } });
74+
fireEvent.change(UI.dateOfBirth(), { target: { value: '10-11-2019' } });
8075

8176
expect(UI.saveButton()).toBeEnabled();
8277
fireEvent.click(UI.saveButton());
@@ -85,7 +80,8 @@ describe('AddUserModal', () => {
8580
firstName: 'John',
8681
lastName: 'Smith',
8782
country: 'Canada',
88-
dateOfBirth: '2019-10-10',
83+
// as unix timestamp
84+
dateOfBirth: moment('10-11-2019', 'DD-MM-YYYY').toDate().getTime(),
8985
});
9086
});
9187

@@ -94,7 +90,7 @@ describe('AddUserModal', () => {
9490
const { UI } = renderAddUserModal({ save: saveHandler });
9591
fireEvent.change(UI.firstName(), { target: { value: 'John' } });
9692
fireEvent.change(UI.lastName(), { target: { value: 'Smith' } });
97-
fireEvent.change(UI.dateOfBirth(), { target: { value: '2019-10-10' } });
93+
fireEvent.change(UI.dateOfBirth(), { target: { value: '10-11-2019' } });
9894
fireEvent.change(UI.country(), { target: { value: 'new' } });
9995
fireEvent.change(UI.newCountry(), { target: { value: 'New Zealand' } });
10096

@@ -105,7 +101,8 @@ describe('AddUserModal', () => {
105101
firstName: 'John',
106102
lastName: 'Smith',
107103
country: 'New Zealand',
108-
dateOfBirth: '2019-10-10',
104+
// as unix timestamp
105+
dateOfBirth: moment('10-11-2019', 'DD-MM-YYYY').toDate().getTime(),
109106
});
110107
});
111108

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import { render, fireEvent, waitFor } from '@testing-library/react';
3+
import RemoveUserModal from 'components/modals/RemoveUserModal';
4+
5+
describe('RemoveUserModal', () => {
6+
it('renders with message', () => {
7+
const { getByText } = render(<RemoveUserModal userId={1} />);
8+
expect(getByText('Do you want to remove user #1?')).toBeInTheDocument();
9+
});
10+
11+
it('reject on No', async () => {
12+
const reject = jest.fn();
13+
const browser = render(<RemoveUserModal rejected={reject} />);
14+
fireEvent.click(browser.getByText('No'));
15+
await waitFor(() => expect(reject).toHaveBeenCalled());
16+
});
17+
18+
it('removeUser on Yes', async () => {
19+
const removeUser = jest.fn();
20+
const browser = render(<RemoveUserModal removeUser={removeUser} />);
21+
fireEvent.click(browser.getByText('Yes'));
22+
await waitFor(() => expect(removeUser).toHaveBeenCalled());
23+
});
24+
});

frontend/src/__tests__/views/Home.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,4 @@ describe('Home', () => {
108108
const { store } = browser;
109109
expect(store.getState().UI.totalPages).toEqual(totalPages);
110110
});
111-
112-
// it('should show a create user modal when the "New user" button is clicked', async () => {
113-
// const browser = renderWithProvider(<Home />);
114-
115-
// // Wait for the data to be fetched and buttons to be displayed
116-
// await waitFor(() => browser.findByText('New user'));
117-
// browser.getByText('New user').click();
118-
119-
// // Check that the modal is displayed
120-
// const modalSelector = 'create-user-modal';
121-
// await waitFor(() => browser.findByTestId(modalSelector));
122-
// expect(browser.getByTestId(modalSelector)).toBeInTheDocument();
123-
// });
124111
});

frontend/src/app.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Almoost whole app is here, except for the intaialization of the store, root div, etc. Used to test the whole app.
2+
import HttpErrorNotification from "components/HttpErrorNotification";
3+
import Routers from "routes/routers";
4+
5+
// styles
6+
import 'styles/index.css';
7+
import 'bootstrap/dist/css/bootstrap.min.css';
8+
9+
export default function App() {
10+
return (
11+
<>
12+
<HttpErrorNotification />
13+
<Routers />
14+
</>
15+
);
16+
}

0 commit comments

Comments
 (0)