epam-ai-conductor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/bin/workshop.js +6 -0
- package/dist/auth-check.d.ts +1 -0
- package/dist/auth-check.js +45 -0
- package/dist/auth-check.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +156 -0
- package/dist/cli.js.map +1 -0
- package/dist/content/content/module-1/task/module-1_home-task_components.md +257 -0
- package/dist/content/content/module-1/task/module-1_home-task_good_practices.md +69 -0
- package/dist/content/content/module-1/task/module-1_home-task_task.md +253 -0
- package/dist/content/content/module-1/tests/Button.test.tsx +22 -0
- package/dist/content/content/module-1/tests/CourseCard.test.tsx +64 -0
- package/dist/content/content/module-1/tests/CourseInfo.test.tsx +73 -0
- package/dist/content/content/module-1/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-1/tests/Header.test.tsx +23 -0
- package/dist/content/content/module-1/tests/Input.test.tsx +36 -0
- package/dist/content/content/module-1/tests/helpers.test.ts +13 -0
- package/dist/content/content/module-1/theory/module-1_elements-render.md +66 -0
- package/dist/content/content/module-1/theory/module-1_hooks_useEffect.md +178 -0
- package/dist/content/content/module-1/theory/module-1_hooks_useState.md +131 -0
- package/dist/content/content/module-1/theory/module-1_react.md +64 -0
- package/dist/content/content/module-1/theory/module-1_spa.md +74 -0
- package/dist/content/content/module-2/task/module-2_home-task_components.md +313 -0
- package/dist/content/content/module-2/task/module-2_home-task_good_practices.md +35 -0
- package/dist/content/content/module-2/task/module-2_home-task_task.md +210 -0
- package/dist/content/content/module-2/tests/App.test.tsx +54 -0
- package/dist/content/content/module-2/tests/Login.test.tsx +73 -0
- package/dist/content/content/module-2/tests/Registration.test.tsx +70 -0
- package/dist/content/content/module-2/tests/SearchBar.test.tsx +39 -0
- package/dist/content/content/module-2/theory/module-2_custom-hooks.md +201 -0
- package/dist/content/content/module-2/theory/module-2_hooks.md +117 -0
- package/dist/content/content/module-2/theory/module-2_react-router.md +328 -0
- package/dist/content/content/module-3/task/module-3_home-task_components.md +94 -0
- package/dist/content/content/module-3/task/module-3_home-task_good_practices.md +26 -0
- package/dist/content/content/module-3/task/module-3_home-task_task.md +170 -0
- package/dist/content/content/module-3/tests/App.test.tsx +54 -0
- package/dist/content/content/module-3/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-3/tests/CreateAuthor.test.tsx +44 -0
- package/dist/content/content/module-3/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/content/module-3/theory/module-3_redux-hooks.md +194 -0
- package/dist/content/content/module-3/theory/module-3_state-actions-reducers.md +445 -0
- package/dist/content/content/module-4/task/module-4_home-task_components.md +187 -0
- package/dist/content/content/module-4/task/module-4_home-task_task.md +139 -0
- package/dist/content/content/module-4/tests/App.test.tsx +54 -0
- package/dist/content/content/module-4/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-4/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/content/module-4/tests/Login.test.tsx +73 -0
- package/dist/content/content/module-4/theory/module-4_async-redux.md +99 -0
- package/dist/content/content/module-4/theory/module-4_private-routes.md +55 -0
- package/dist/content/content/module-5/task/module-5_home-task_instruction.md +68 -0
- package/dist/content/content/module-5/task/module-5_home-task_task.md +154 -0
- package/dist/content/content/module-5/tests/App.test.tsx +54 -0
- package/dist/content/content/module-5/tests/CourseCard.test.tsx +64 -0
- package/dist/content/content/module-5/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-5/tests/Header.test.tsx +23 -0
- package/dist/content/content/module-5/theory/module-5_react-testing-library_example.md +379 -0
- package/dist/content/content/module-5/theory/module-5_redux-writing-tests.md +246 -0
- package/dist/content/module-1/task/module-1_home-task_components.md +257 -0
- package/dist/content/module-1/task/module-1_home-task_good_practices.md +69 -0
- package/dist/content/module-1/task/module-1_home-task_task.md +253 -0
- package/dist/content/module-1/tests/Button.test.tsx +22 -0
- package/dist/content/module-1/tests/CourseCard.test.tsx +64 -0
- package/dist/content/module-1/tests/CourseInfo.test.tsx +73 -0
- package/dist/content/module-1/tests/Courses.test.tsx +87 -0
- package/dist/content/module-1/tests/Header.test.tsx +23 -0
- package/dist/content/module-1/tests/Input.test.tsx +36 -0
- package/dist/content/module-1/tests/helpers.test.ts +13 -0
- package/dist/content/module-1/theory/module-1_elements-render.md +66 -0
- package/dist/content/module-1/theory/module-1_hooks_useEffect.md +178 -0
- package/dist/content/module-1/theory/module-1_hooks_useState.md +131 -0
- package/dist/content/module-1/theory/module-1_react.md +64 -0
- package/dist/content/module-1/theory/module-1_spa.md +74 -0
- package/dist/content/module-2/task/module-2_home-task_components.md +313 -0
- package/dist/content/module-2/task/module-2_home-task_good_practices.md +35 -0
- package/dist/content/module-2/task/module-2_home-task_task.md +210 -0
- package/dist/content/module-2/tests/App.test.tsx +54 -0
- package/dist/content/module-2/tests/Login.test.tsx +73 -0
- package/dist/content/module-2/tests/Registration.test.tsx +70 -0
- package/dist/content/module-2/tests/SearchBar.test.tsx +39 -0
- package/dist/content/module-2/theory/module-2_custom-hooks.md +201 -0
- package/dist/content/module-2/theory/module-2_hooks.md +117 -0
- package/dist/content/module-2/theory/module-2_react-router.md +328 -0
- package/dist/content/module-3/task/module-3_home-task_components.md +94 -0
- package/dist/content/module-3/task/module-3_home-task_good_practices.md +26 -0
- package/dist/content/module-3/task/module-3_home-task_task.md +170 -0
- package/dist/content/module-3/tests/App.test.tsx +54 -0
- package/dist/content/module-3/tests/Courses.test.tsx +87 -0
- package/dist/content/module-3/tests/CreateAuthor.test.tsx +44 -0
- package/dist/content/module-3/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/module-3/theory/module-3_redux-hooks.md +194 -0
- package/dist/content/module-3/theory/module-3_state-actions-reducers.md +445 -0
- package/dist/content/module-4/task/module-4_home-task_components.md +187 -0
- package/dist/content/module-4/task/module-4_home-task_task.md +139 -0
- package/dist/content/module-4/tests/App.test.tsx +54 -0
- package/dist/content/module-4/tests/Courses.test.tsx +87 -0
- package/dist/content/module-4/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/module-4/tests/Login.test.tsx +73 -0
- package/dist/content/module-4/theory/module-4_async-redux.md +99 -0
- package/dist/content/module-4/theory/module-4_private-routes.md +55 -0
- package/dist/content/module-5/task/module-5_home-task_instruction.md +68 -0
- package/dist/content/module-5/task/module-5_home-task_task.md +154 -0
- package/dist/content/module-5/tests/App.test.tsx +54 -0
- package/dist/content/module-5/tests/CourseCard.test.tsx +64 -0
- package/dist/content/module-5/tests/Courses.test.tsx +87 -0
- package/dist/content/module-5/tests/Header.test.tsx +23 -0
- package/dist/content/module-5/theory/module-5_react-testing-library_example.md +379 -0
- package/dist/content/module-5/theory/module-5_redux-writing-tests.md +246 -0
- package/dist/content-loader.d.ts +7 -0
- package/dist/content-loader.js +26 -0
- package/dist/content-loader.js.map +1 -0
- package/dist/context-builder.d.ts +2 -0
- package/dist/context-builder.js +116 -0
- package/dist/context-builder.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 1
|
|
3
|
+
sidebar_label: 'TASK'
|
|
4
|
+
title: 'Module 4. Async Redux'
|
|
5
|
+
---
|
|
6
|
+
This is the fourth part of the application.
|
|
7
|
+
In this assignment,
|
|
8
|
+
you should continue to work on your project with a slight modification.
|
|
9
|
+
This module designed to teach how to work with asynchronously Redux.
|
|
10
|
+
|
|
11
|
+
## Prerequisite:
|
|
12
|
+
|
|
13
|
+
:::info
|
|
14
|
+
You don't need to install `redux-thunk` because by default `@reduxjs/toolkit` includes it.
|
|
15
|
+
:::
|
|
16
|
+
|
|
17
|
+
## Project structure requirements
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
1. Create a new folder and `jsx / tsx` file for new components:
|
|
21
|
+
* src / components / PrivateRoute / **PrivateRoute.jsx/.tsx**
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
2. Rename `CreateCourse` folder and file:
|
|
25
|
+
* src / components / CreateCourse / CreateCourse.jsx/.tsx => CourseForm / **CourseForm.jsx/.tsx**
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
3. Create new `js/ts` files for thunk functions:
|
|
29
|
+
```
|
|
30
|
+
src
|
|
31
|
+
|-- store
|
|
32
|
+
| |
|
|
33
|
+
| |-- user
|
|
34
|
+
| | |-- thunk.js/.ts //new file
|
|
35
|
+
| | |
|
|
36
|
+
| | |__ //old files
|
|
37
|
+
| |
|
|
38
|
+
| |-- courses
|
|
39
|
+
| | |-- thunk.js/.ts //new file
|
|
40
|
+
| | |
|
|
41
|
+
| | |__ // old files
|
|
42
|
+
| |
|
|
43
|
+
| |__ authors
|
|
44
|
+
| |-- thunk.js/.ts //new file
|
|
45
|
+
| |
|
|
46
|
+
| |__ //old files
|
|
47
|
+
|
|
|
48
|
+
|__ ...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
4. Admin credentials. These credentials provide role **ADMIN**.
|
|
53
|
+
```javascript
|
|
54
|
+
email: admin@email.com
|
|
55
|
+
password: admin123
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
5. APIs from SWAGGER for Module 4:
|
|
59
|
+
- `/courses/add` [POST] (use ADMIN credentials)
|
|
60
|
+
- `/courses/{id}` [PUT] (use ADMIN credentials)
|
|
61
|
+
- `/authors/add` [POST] (use ADMIN credentials)
|
|
62
|
+
- `/courses/{id}` [DELETE] (use ADMIN credentials)
|
|
63
|
+
- `/logout` [DELETE]
|
|
64
|
+
- `/users/me` [GET]
|
|
65
|
+
|
|
66
|
+
:::info
|
|
67
|
+
For fetching requests you should add `Authorization` header with user's token
|
|
68
|
+
`Authorization: token`
|
|
69
|
+
:::
|
|
70
|
+
|
|
71
|
+
## Criteria (30 points max)
|
|
72
|
+
|
|
73
|
+
### APIs
|
|
74
|
+
|
|
75
|
+
* [8 points] - Use **Thunk** for asynchronous actions. Rewrite old GET requests `/courses/all` and `/authors/all`.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
* [2 points] - Create request to get current user if localStorage contains token. (See `/users/me [GET]` in the SWAGGER).
|
|
79
|
+
|
|
80
|
+
### PrivateRoute Component
|
|
81
|
+
* [2 points] - Create new component `PrivateRoute`.
|
|
82
|
+
Use [`PrivateRoute`](/docs/module-4/home-task/components#private-route-new-component) for routes:
|
|
83
|
+
- `/courses/add`;
|
|
84
|
+
- `/courses/update/:courseId`.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
### Header
|
|
88
|
+
* [2 points] - Modify **Logout** functionality. Call endpoint for loggout on a back-end. (See `/logout` in the SWAGGER)
|
|
89
|
+
|
|
90
|
+
### CourseCard Component
|
|
91
|
+
* [1 point] - Show buttons `Delete course` and `Update course` when role - ADMIN.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
* [2 points] - Sent delete request to the back-end. (See `/courses/{id}` in the SWAGGER).
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
* [1 point] - Delete the course from the **store** after success API response.
|
|
98
|
+
*(**Course card with ADMIN role** in COMPONENTS.pdf)*.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
### CourseForm Component (create mode)
|
|
102
|
+
|
|
103
|
+
* [1 point] - Show `/courses/add` route only for user with **ADMIN** role.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
* [2 points] - Send new course to the back-end. (See `/courses/add` in the SWAGGER).
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
* [2 points] - Send new author to the back-end. (See `/authors/add` in the SWAGGER).
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
* [1 point] - Save new course to the **store** after success response.
|
|
113
|
+
*(**Add new course** in COMPONENTS)*.
|
|
114
|
+
|
|
115
|
+
### CourseForm Component (update mode)
|
|
116
|
+
* [1 point] - `Update Course` form should be opened by route `/courses/update/:courseId`.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
* [1 point] - Show route only for user with **ADMIN** role.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
* [1 point] - Show information about course in fields with the same name.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
* [2 points] - Send updated course to the back-end. (See [PUT] `/courses/{id}` in the SWAGGER).
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
* [1 point] - Save updated course to the **store** after success response.
|
|
129
|
+
*(**Update course** in COMPONENTS)*.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
### [EmptyCourseList Component](/docs/module-4/home-task/components#emptycourselist-component)
|
|
133
|
+
|
|
134
|
+
* [4 point] - Navigate to `/courses/add` FOR ADMIN USER or show special message for REGULAR USER by clicking `Add New Course` button.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
:::info
|
|
138
|
+
Please find detailed description of components and functionality in the [COMPONENTS](components) section.
|
|
139
|
+
:::
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { MemoryRouter } from "react-router-dom";
|
|
4
|
+
import App from "../App";
|
|
5
|
+
|
|
6
|
+
describe("App", () => {
|
|
7
|
+
test("renders Login component when token is not present", () => {
|
|
8
|
+
localStorage.removeItem("token");
|
|
9
|
+
render(
|
|
10
|
+
<MemoryRouter initialEntries={["/"]}>
|
|
11
|
+
<App />
|
|
12
|
+
</MemoryRouter>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(
|
|
16
|
+
screen.getByText(/If you don't have an account you can/)
|
|
17
|
+
).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("renders Courses component when token is present", () => {
|
|
21
|
+
localStorage.setItem("token", "token");
|
|
22
|
+
render(
|
|
23
|
+
<MemoryRouter initialEntries={["/"]}>
|
|
24
|
+
<App />
|
|
25
|
+
</MemoryRouter>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const courseElements = screen.getAllByTestId("courseCard");
|
|
29
|
+
|
|
30
|
+
expect(courseElements[0]).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("renders Registration component", () => {
|
|
34
|
+
render(
|
|
35
|
+
<MemoryRouter initialEntries={["/registration"]}>
|
|
36
|
+
<App />
|
|
37
|
+
</MemoryRouter>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(screen.getByLabelText("name")).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByLabelText("email")).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByLabelText("password")).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("renders course add component", () => {
|
|
46
|
+
render(
|
|
47
|
+
<MemoryRouter initialEntries={["/courses/add"]}>
|
|
48
|
+
<App />
|
|
49
|
+
</MemoryRouter>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByTestId("titleInput")).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Courses } from "../components/Courses/Courses";
|
|
4
|
+
|
|
5
|
+
describe('Courses component', () => {
|
|
6
|
+
const mockedCoursesList = [
|
|
7
|
+
{
|
|
8
|
+
id: "1",
|
|
9
|
+
title: "Course 1",
|
|
10
|
+
description: "Course 1 description",
|
|
11
|
+
creationDate: "2022-01-01",
|
|
12
|
+
duration: 60,
|
|
13
|
+
authors: [
|
|
14
|
+
"27cc3006-e93a-4748-8ca8-73d06aa93b6d",
|
|
15
|
+
"f762978b-61eb-4096-812b-ebde22838167",
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "2",
|
|
20
|
+
title: "Course 2",
|
|
21
|
+
description: "Course 2 description",
|
|
22
|
+
creationDate: "2022-02-01",
|
|
23
|
+
duration: 90,
|
|
24
|
+
authors: ["df32994e-b23d-497c-9e4d-84e4dc02882f"],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
const onAddClick = jest.fn();
|
|
28
|
+
|
|
29
|
+
test("Should render list of courses", () => {
|
|
30
|
+
render(<Courses courseList={mockedCoursesList} onAddClick={onAddClick} setSelectedCourseId={(id) => {}}/>);
|
|
31
|
+
|
|
32
|
+
const courseElements = screen.getAllByTestId("courseCard");
|
|
33
|
+
|
|
34
|
+
expect(courseElements[0]).toBeInTheDocument();
|
|
35
|
+
expect(courseElements).toHaveLength(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("Should render 'Add new course' button", () => {
|
|
39
|
+
render(<Courses courseList={mockedCoursesList} onAddClick={onAddClick} setSelectedCourseId={(id) => {}}/>);
|
|
40
|
+
|
|
41
|
+
const button = screen.getByText(/Add new course/);
|
|
42
|
+
|
|
43
|
+
expect(button).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("Should render EmptyCourseList component if no courses", () => {
|
|
47
|
+
const mockedEmptyCoursesList: [] = [];
|
|
48
|
+
render(<Courses courseList={mockedEmptyCoursesList} onAddClick={onAddClick} setSelectedCourseId={(id) => {}}/>);
|
|
49
|
+
|
|
50
|
+
const emptyContainerElement = screen.getByTestId("emptyContainer");
|
|
51
|
+
const addButtonElement = screen.getByTestId("addCourse");
|
|
52
|
+
|
|
53
|
+
expect(emptyContainerElement).toBeInTheDocument();
|
|
54
|
+
expect(addButtonElement).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('EmptyCourseList', () => {
|
|
59
|
+
const mockedEmptyCoursesList: [] = [];
|
|
60
|
+
const onAddClick = jest.fn();
|
|
61
|
+
|
|
62
|
+
test("Should render title", () => {
|
|
63
|
+
render(<Courses courseList={mockedEmptyCoursesList} onAddClick={onAddClick} setSelectedCourseId={(id) => {}}/>);
|
|
64
|
+
|
|
65
|
+
const title = screen.getByText(/Your List Is Empty/);
|
|
66
|
+
|
|
67
|
+
expect(title).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("Should render subtitle", () => {
|
|
71
|
+
render(<Courses courseList={mockedEmptyCoursesList} onAddClick={onAddClick} setSelectedCourseId={(id) => {}}/>);
|
|
72
|
+
|
|
73
|
+
const subTitle = screen.getByText(/Please use/);
|
|
74
|
+
|
|
75
|
+
expect(subTitle).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("Should render button", () => {
|
|
79
|
+
render(<Courses courseList={mockedEmptyCoursesList} onAddClick={onAddClick} setSelectedCourseId={(id) => {}}/>);
|
|
80
|
+
|
|
81
|
+
const button = screen.getByText('ADD NEW COURSE');
|
|
82
|
+
|
|
83
|
+
expect(button).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, fireEvent, screen } from "@testing-library/react";
|
|
3
|
+
import { CreateCourse } from "../components/CreateCourse/CreateCourse";
|
|
4
|
+
|
|
5
|
+
describe("CreateCourse", () => {
|
|
6
|
+
const mockAuthors = [
|
|
7
|
+
{ id: "1", name: "Author 1" },
|
|
8
|
+
{ id: "2", name: "Author 2" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const mockCreateCourse = jest.fn();
|
|
12
|
+
const mockCreateAuthor = jest.fn();
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockCreateCourse.mockClear();
|
|
16
|
+
mockCreateAuthor.mockClear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should create a new course with the entered data", () => {
|
|
20
|
+
render(
|
|
21
|
+
<CreateCourse
|
|
22
|
+
authorsList={mockAuthors}
|
|
23
|
+
createCourse={mockCreateCourse}
|
|
24
|
+
createAuthor={mockCreateAuthor}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const titleInput = screen.getByTestId("titleInput");
|
|
29
|
+
const durationInput = screen.getByTestId("durationInput");
|
|
30
|
+
const descriptionTextArea = screen.getByTestId("descriptionTextArea");
|
|
31
|
+
const createCourseButton = screen.getByTestId("createCourseButton");
|
|
32
|
+
|
|
33
|
+
// Enter the course title
|
|
34
|
+
fireEvent.change(titleInput, { target: { value: "Course Title" } });
|
|
35
|
+
fireEvent.change(durationInput, { target: { value: "20" } });
|
|
36
|
+
|
|
37
|
+
// Enter the course description
|
|
38
|
+
fireEvent.change(descriptionTextArea, {
|
|
39
|
+
target: { value: "Course Description" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Click the create course button
|
|
43
|
+
fireEvent.click(createCourseButton);
|
|
44
|
+
|
|
45
|
+
// Assert that createCourse is called with the correct data
|
|
46
|
+
expect(mockCreateCourse).toHaveBeenCalledWith({
|
|
47
|
+
id: expect.any(String),
|
|
48
|
+
title: "Course Title",
|
|
49
|
+
description: "Course Description",
|
|
50
|
+
creationDate: "",
|
|
51
|
+
duration: 20,
|
|
52
|
+
authors: [],
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should not create a new course with the empty data", () => {
|
|
57
|
+
render(
|
|
58
|
+
<CreateCourse
|
|
59
|
+
authorsList={mockAuthors}
|
|
60
|
+
createCourse={mockCreateCourse}
|
|
61
|
+
createAuthor={mockCreateAuthor}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const createCourseButton = screen.getByTestId("createCourseButton");
|
|
66
|
+
|
|
67
|
+
// Click the create course button
|
|
68
|
+
fireEvent.click(createCourseButton);
|
|
69
|
+
|
|
70
|
+
// Assert that createCourse is called with the correct data
|
|
71
|
+
expect(mockCreateCourse).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should add an author to the course when CreateAuthor component creates a new author", () => {
|
|
75
|
+
render(
|
|
76
|
+
<CreateCourse
|
|
77
|
+
authorsList={mockAuthors}
|
|
78
|
+
createCourse={mockCreateCourse}
|
|
79
|
+
createAuthor={mockCreateAuthor}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const createAuthorButton = screen.getByTestId("createAuthorButton");
|
|
84
|
+
const nameInput = screen.getByTestId(
|
|
85
|
+
"createAuthorInput"
|
|
86
|
+
) as HTMLInputElement;
|
|
87
|
+
|
|
88
|
+
fireEvent.change(nameInput, { target: { value: "John" } });
|
|
89
|
+
|
|
90
|
+
fireEvent.click(createAuthorButton);
|
|
91
|
+
|
|
92
|
+
expect(mockCreateAuthor).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should display the course authors in the course", () => {
|
|
96
|
+
const mockCourseAuthors = ["1", "2"];
|
|
97
|
+
render(
|
|
98
|
+
<CreateCourse
|
|
99
|
+
authorsList={mockAuthors}
|
|
100
|
+
createCourse={mockCreateCourse}
|
|
101
|
+
createAuthor={mockCreateAuthor}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const courseAuthorItems = screen.getAllByTestId("authorItem");
|
|
106
|
+
expect(courseAuthorItems.length).toBe(mockCourseAuthors.length);
|
|
107
|
+
});
|
|
108
|
+
it("should display the empty authors list message when there are no authors", () => {
|
|
109
|
+
render(
|
|
110
|
+
<CreateCourse
|
|
111
|
+
authorsList={[]}
|
|
112
|
+
createCourse={mockCreateCourse}
|
|
113
|
+
createAuthor={mockCreateAuthor}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
const emptyText = screen.getByTestId("emptyText");
|
|
117
|
+
|
|
118
|
+
// Assert that the empty text is displayed
|
|
119
|
+
expect(emptyText).toBeInTheDocument();
|
|
120
|
+
expect(emptyText.textContent).toBe("Authors list is empty");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, fireEvent, screen } from "@testing-library/react";
|
|
3
|
+
import { MemoryRouter } from "react-router-dom";
|
|
4
|
+
import { Login } from "../components/Login";
|
|
5
|
+
import * as services from "../../../services";
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.spyOn(services, "login").mockImplementation(
|
|
9
|
+
jest.fn(() =>
|
|
10
|
+
Promise.resolve({
|
|
11
|
+
result: "token",
|
|
12
|
+
user: {
|
|
13
|
+
name: "John Doe",
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
) as jest.Mock
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
jest.spyOn(global, "fetch").mockImplementation(
|
|
20
|
+
jest.fn(() =>
|
|
21
|
+
Promise.resolve({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: () =>
|
|
24
|
+
Promise.resolve({
|
|
25
|
+
result: "token",
|
|
26
|
+
user: {
|
|
27
|
+
name: "John Doe",
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
})
|
|
31
|
+
) as jest.Mock
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("Login", () => {
|
|
36
|
+
test("renders login form", () => {
|
|
37
|
+
render(
|
|
38
|
+
<MemoryRouter>
|
|
39
|
+
<Login />
|
|
40
|
+
</MemoryRouter>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(screen.getByLabelText("email")).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByLabelText("password")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText("Login")).toBeInTheDocument();
|
|
46
|
+
expect(
|
|
47
|
+
screen.getByText(/If you don't have an account you can/)
|
|
48
|
+
).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByRole("link")).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("submits login form", async () => {
|
|
53
|
+
render(
|
|
54
|
+
<MemoryRouter>
|
|
55
|
+
<Login />
|
|
56
|
+
</MemoryRouter>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
fireEvent.change(screen.getByLabelText("email"), {
|
|
60
|
+
target: { value: "test@example.com" },
|
|
61
|
+
});
|
|
62
|
+
fireEvent.change(screen.getByLabelText("password"), {
|
|
63
|
+
target: { value: "password123" },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
fireEvent.click(screen.getByText("login"));
|
|
67
|
+
|
|
68
|
+
expect(services.login).toHaveBeenCalledWith({
|
|
69
|
+
email: "test@example.com",
|
|
70
|
+
password: "password123",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 1
|
|
3
|
+
sidebar_label: "Async Redux"
|
|
4
|
+
title: "Async Redux"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
In previous module we have talked about Redux and its possibilities for better moduling your application. Our code
|
|
8
|
+
looks much better, but here comes another problem: if you want to fetch some data from api - you need to make async
|
|
9
|
+
call to continue app executions - and when data is received, dispatch some action.
|
|
10
|
+
|
|
11
|
+
**In this tutorial we will discuss
|
|
12
|
+
how to include async login into Redux app.**
|
|
13
|
+
|
|
14
|
+
## Intro into Middleware
|
|
15
|
+
|
|
16
|
+
Earlier we said, that reducer has not to have side effect, but what if we do? That's why middleware was created.
|
|
17
|
+
Middleware is also a function, like a bridge between call of dispatch action and reducer. In middleware, we can log
|
|
18
|
+
something, delay the action call. <br/>
|
|
19
|
+
We can also interact with redux, making async calls. Furthermore, since middleware is
|
|
20
|
+
not reducer, we can pass to it functions to make available async flow.
|
|
21
|
+
|
|
22
|
+
## Creating middleware function
|
|
23
|
+
|
|
24
|
+
To begin, lets first create middleware in our store.
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const asyncFunctionMiddleware = (storeAPI) => (next) => (action) => {
|
|
28
|
+
// If the "action" is actually a function instead...
|
|
29
|
+
if (typeof action === "function") {
|
|
30
|
+
// then call the function and pass `dispatch` and `getState` as arguments
|
|
31
|
+
return action(storeAPI.dispatch, storeAPI.getState);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Otherwise, it's a normal action - send it onwards
|
|
35
|
+
return next(action);
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The function above describes the purpose of middleware function. It checks first if passed argument is a function, when
|
|
40
|
+
it's true, call the function.
|
|
41
|
+
Now, to enable async flow:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { applyMiddleware } from "redux";
|
|
45
|
+
const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware);
|
|
46
|
+
const store = createStore(rootReducer, middlewareEnhancer);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
:::info
|
|
50
|
+
This "Async function middleware" let us pass a function to dispatch.
|
|
51
|
+
Inside that function, we are able to write some async logic (an HTTP request).
|
|
52
|
+
:::
|
|
53
|
+
|
|
54
|
+
## Example of usage
|
|
55
|
+
|
|
56
|
+
In next step we will create method, that will make api request and when response will come - we dispatch action to store.
|
|
57
|
+
Imagine, that we are working with library  system and want to display on page all
|
|
58
|
+
available books for users.
|
|
59
|
+
|
|
60
|
+
To code this, add a function:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const fetchBooks = async (dispatch, getState) => {
|
|
64
|
+
const books = await api.get("books");
|
|
65
|
+
console.log(books);
|
|
66
|
+
dispatch({ type: "addBooks", payload: { books: books } });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
store.dispatch(fetchBooks);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
You can see, that our function `fetchBooks` takes arguments `dispatch` and `getState` - this is where middleware comes.
|
|
73
|
+
In client code, we only call `store.dispatch(fetchBooks)` and middleware forward to this method automatically. Simple and
|
|
74
|
+
clean, right?
|
|
75
|
+
|
|
76
|
+
The whole process looks like this:
|
|
77
|
+

|
|
78
|
+
Look closer at gif, that describes in details Redux async flow using special library `thunk`. We will discuss it in next
|
|
79
|
+
section (shortly speaking it's a Redux middleware).
|
|
80
|
+
|
|
81
|
+
:::info
|
|
82
|
+
You can write your application code without middleware, its just syntax sugar. Middleware allows you to do some side effects.
|
|
83
|
+
:::
|
|
84
|
+
|
|
85
|
+
## Summary
|
|
86
|
+
|
|
87
|
+
So, to repeat all steps:
|
|
88
|
+
|
|
89
|
+
- store dispatch an action, for example when user clicked on button;
|
|
90
|
+
- middleware looks for a function, that we have passed into dispatch;
|
|
91
|
+
- in function, we can do some async call, to get data from api;
|
|
92
|
+
- when we get all needed data, we call dispatch action, that will update the store with new data.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Materials
|
|
97
|
+
|
|
98
|
+
1. [Redux async logic](https://redux.js.org/tutorials/fundamentals/part-6-async-logic)
|
|
99
|
+
2. [Redux Tutorial - Learn Redux from Scratch (Video)](https://www.youtube.com/watch?v=poQXNp9ItL4&t=3450s)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 3
|
|
3
|
+
sidebar_label: "Create Private Routes"
|
|
4
|
+
title: "Private Routes"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why Using Private Routes?
|
|
8
|
+
|
|
9
|
+
Sometimes you need to hide some routes from users depending on the conditions.
|
|
10
|
+
These can be **private routes** that are only available to admins or pages that can only be viewed
|
|
11
|
+
by authorized users.
|
|
12
|
+
The best approach to solving such a problem is to build a component that will have the appropriate logic.
|
|
13
|
+
|
|
14
|
+
Why use a separate component for this?
|
|
15
|
+
Why not just add the appropriate condition for each route?
|
|
16
|
+
It's simple: **readability** and **composition**.
|
|
17
|
+
The presence of such a component will make the routing code **more understandable**,
|
|
18
|
+
and such a component is very convenient to **reuse**.
|
|
19
|
+
|
|
20
|
+
## Example of PrivateRoute Component
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
// PrivateRoute.jsx
|
|
24
|
+
|
|
25
|
+
function PrivateRoute({ children }) {
|
|
26
|
+
const auth = getUserRole(); // add logic to determine the value for the condition
|
|
27
|
+
|
|
28
|
+
return auth ? children : <Navigate to="/login" />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// App.jsx
|
|
32
|
+
function App() {
|
|
33
|
+
return (
|
|
34
|
+
<BrowserRouter>
|
|
35
|
+
<Routes>
|
|
36
|
+
<Route path="/" element={[your public component]} />
|
|
37
|
+
<Route
|
|
38
|
+
path="/private"
|
|
39
|
+
element={
|
|
40
|
+
<PrivateRoute>
|
|
41
|
+
[your private component]
|
|
42
|
+
</PrivateRoute>
|
|
43
|
+
}
|
|
44
|
+
/>
|
|
45
|
+
</Routes>
|
|
46
|
+
</BrowserRouter>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It's look like very easy way to manage your routes, isn't it? 😀
|
|
52
|
+
|
|
53
|
+
## Materials
|
|
54
|
+
|
|
55
|
+
1. [Private Route in react-router v6](https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 2
|
|
3
|
+
sidebar_label: 'INSTRUCTIONS'
|
|
4
|
+
title: Instructions
|
|
5
|
+
---
|
|
6
|
+
# Instruction for test writing
|
|
7
|
+
**You should never use real data from the server for testing. Use only mocked data.**
|
|
8
|
+
## Reducer testing
|
|
9
|
+
*A reducer is a function that takes state and action and returns a new state,
|
|
10
|
+
so you can test it like general function.*
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
- Go to the [link](https://redux.js.org/usage/writing-tests). Find the direct example for test `should return the initial state`.
|
|
14
|
+
|
|
15
|
+
:::note
|
|
16
|
+
You don't need to install `Babel`
|
|
17
|
+
:::
|
|
18
|
+
|
|
19
|
+
- Use this example for writing your tests.
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Components testing
|
|
23
|
+
To test the React component you need to render this component in the test.
|
|
24
|
+
Use [link](https://testing-library.com/docs/react-testing-library/example-intro) to see example of rendering.
|
|
25
|
+
If you use store data in testing component you need to cover component in the `Provider` and pass the `mocked store`.
|
|
26
|
+
|
|
27
|
+
*Example of mockedStore:*
|
|
28
|
+
```js
|
|
29
|
+
const mockedState = {
|
|
30
|
+
user: {
|
|
31
|
+
isAuth: true,
|
|
32
|
+
name: 'Test Name',
|
|
33
|
+
},
|
|
34
|
+
courses: [],
|
|
35
|
+
authors: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const mockedStore = {
|
|
39
|
+
getState: () => mockedState,
|
|
40
|
+
subscribe: jest.fn(),
|
|
41
|
+
dispatch: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
*Example of using mockedStore in the test:*
|
|
46
|
+
```js
|
|
47
|
+
render(
|
|
48
|
+
<Provider store={mockedStore}>
|
|
49
|
+
<Header />
|
|
50
|
+
</Provider>
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use `.toBeInTheDocument()` to check if something is on the page.
|
|
55
|
+
|
|
56
|
+
*Example of expectation*:
|
|
57
|
+
```js
|
|
58
|
+
expect(screen.queryByText('Test Name')).toBeInTheDocument();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Use [this link](https://testing-library.com/docs/vue-testing-library/cheatsheet/#queries) to see different queries.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
- To simulate events *('click' for buttons and 'change' for input)* you can use [Firing Events](https://testing-library.com/docs/dom-testing-library/api-events).
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
- Read ["How To Mock Fetch in Jest" article](https://www.leighhalliday.com/mock-fetch-jest) to learn how to mock fetch.
|
|
68
|
+
|