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.
Files changed (115) hide show
  1. package/README.md +46 -0
  2. package/bin/workshop.js +6 -0
  3. package/dist/auth-check.d.ts +1 -0
  4. package/dist/auth-check.js +45 -0
  5. package/dist/auth-check.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +156 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/content/content/module-1/task/module-1_home-task_components.md +257 -0
  10. package/dist/content/content/module-1/task/module-1_home-task_good_practices.md +69 -0
  11. package/dist/content/content/module-1/task/module-1_home-task_task.md +253 -0
  12. package/dist/content/content/module-1/tests/Button.test.tsx +22 -0
  13. package/dist/content/content/module-1/tests/CourseCard.test.tsx +64 -0
  14. package/dist/content/content/module-1/tests/CourseInfo.test.tsx +73 -0
  15. package/dist/content/content/module-1/tests/Courses.test.tsx +87 -0
  16. package/dist/content/content/module-1/tests/Header.test.tsx +23 -0
  17. package/dist/content/content/module-1/tests/Input.test.tsx +36 -0
  18. package/dist/content/content/module-1/tests/helpers.test.ts +13 -0
  19. package/dist/content/content/module-1/theory/module-1_elements-render.md +66 -0
  20. package/dist/content/content/module-1/theory/module-1_hooks_useEffect.md +178 -0
  21. package/dist/content/content/module-1/theory/module-1_hooks_useState.md +131 -0
  22. package/dist/content/content/module-1/theory/module-1_react.md +64 -0
  23. package/dist/content/content/module-1/theory/module-1_spa.md +74 -0
  24. package/dist/content/content/module-2/task/module-2_home-task_components.md +313 -0
  25. package/dist/content/content/module-2/task/module-2_home-task_good_practices.md +35 -0
  26. package/dist/content/content/module-2/task/module-2_home-task_task.md +210 -0
  27. package/dist/content/content/module-2/tests/App.test.tsx +54 -0
  28. package/dist/content/content/module-2/tests/Login.test.tsx +73 -0
  29. package/dist/content/content/module-2/tests/Registration.test.tsx +70 -0
  30. package/dist/content/content/module-2/tests/SearchBar.test.tsx +39 -0
  31. package/dist/content/content/module-2/theory/module-2_custom-hooks.md +201 -0
  32. package/dist/content/content/module-2/theory/module-2_hooks.md +117 -0
  33. package/dist/content/content/module-2/theory/module-2_react-router.md +328 -0
  34. package/dist/content/content/module-3/task/module-3_home-task_components.md +94 -0
  35. package/dist/content/content/module-3/task/module-3_home-task_good_practices.md +26 -0
  36. package/dist/content/content/module-3/task/module-3_home-task_task.md +170 -0
  37. package/dist/content/content/module-3/tests/App.test.tsx +54 -0
  38. package/dist/content/content/module-3/tests/Courses.test.tsx +87 -0
  39. package/dist/content/content/module-3/tests/CreateAuthor.test.tsx +44 -0
  40. package/dist/content/content/module-3/tests/CreateCourse.test.tsx +122 -0
  41. package/dist/content/content/module-3/theory/module-3_redux-hooks.md +194 -0
  42. package/dist/content/content/module-3/theory/module-3_state-actions-reducers.md +445 -0
  43. package/dist/content/content/module-4/task/module-4_home-task_components.md +187 -0
  44. package/dist/content/content/module-4/task/module-4_home-task_task.md +139 -0
  45. package/dist/content/content/module-4/tests/App.test.tsx +54 -0
  46. package/dist/content/content/module-4/tests/Courses.test.tsx +87 -0
  47. package/dist/content/content/module-4/tests/CreateCourse.test.tsx +122 -0
  48. package/dist/content/content/module-4/tests/Login.test.tsx +73 -0
  49. package/dist/content/content/module-4/theory/module-4_async-redux.md +99 -0
  50. package/dist/content/content/module-4/theory/module-4_private-routes.md +55 -0
  51. package/dist/content/content/module-5/task/module-5_home-task_instruction.md +68 -0
  52. package/dist/content/content/module-5/task/module-5_home-task_task.md +154 -0
  53. package/dist/content/content/module-5/tests/App.test.tsx +54 -0
  54. package/dist/content/content/module-5/tests/CourseCard.test.tsx +64 -0
  55. package/dist/content/content/module-5/tests/Courses.test.tsx +87 -0
  56. package/dist/content/content/module-5/tests/Header.test.tsx +23 -0
  57. package/dist/content/content/module-5/theory/module-5_react-testing-library_example.md +379 -0
  58. package/dist/content/content/module-5/theory/module-5_redux-writing-tests.md +246 -0
  59. package/dist/content/module-1/task/module-1_home-task_components.md +257 -0
  60. package/dist/content/module-1/task/module-1_home-task_good_practices.md +69 -0
  61. package/dist/content/module-1/task/module-1_home-task_task.md +253 -0
  62. package/dist/content/module-1/tests/Button.test.tsx +22 -0
  63. package/dist/content/module-1/tests/CourseCard.test.tsx +64 -0
  64. package/dist/content/module-1/tests/CourseInfo.test.tsx +73 -0
  65. package/dist/content/module-1/tests/Courses.test.tsx +87 -0
  66. package/dist/content/module-1/tests/Header.test.tsx +23 -0
  67. package/dist/content/module-1/tests/Input.test.tsx +36 -0
  68. package/dist/content/module-1/tests/helpers.test.ts +13 -0
  69. package/dist/content/module-1/theory/module-1_elements-render.md +66 -0
  70. package/dist/content/module-1/theory/module-1_hooks_useEffect.md +178 -0
  71. package/dist/content/module-1/theory/module-1_hooks_useState.md +131 -0
  72. package/dist/content/module-1/theory/module-1_react.md +64 -0
  73. package/dist/content/module-1/theory/module-1_spa.md +74 -0
  74. package/dist/content/module-2/task/module-2_home-task_components.md +313 -0
  75. package/dist/content/module-2/task/module-2_home-task_good_practices.md +35 -0
  76. package/dist/content/module-2/task/module-2_home-task_task.md +210 -0
  77. package/dist/content/module-2/tests/App.test.tsx +54 -0
  78. package/dist/content/module-2/tests/Login.test.tsx +73 -0
  79. package/dist/content/module-2/tests/Registration.test.tsx +70 -0
  80. package/dist/content/module-2/tests/SearchBar.test.tsx +39 -0
  81. package/dist/content/module-2/theory/module-2_custom-hooks.md +201 -0
  82. package/dist/content/module-2/theory/module-2_hooks.md +117 -0
  83. package/dist/content/module-2/theory/module-2_react-router.md +328 -0
  84. package/dist/content/module-3/task/module-3_home-task_components.md +94 -0
  85. package/dist/content/module-3/task/module-3_home-task_good_practices.md +26 -0
  86. package/dist/content/module-3/task/module-3_home-task_task.md +170 -0
  87. package/dist/content/module-3/tests/App.test.tsx +54 -0
  88. package/dist/content/module-3/tests/Courses.test.tsx +87 -0
  89. package/dist/content/module-3/tests/CreateAuthor.test.tsx +44 -0
  90. package/dist/content/module-3/tests/CreateCourse.test.tsx +122 -0
  91. package/dist/content/module-3/theory/module-3_redux-hooks.md +194 -0
  92. package/dist/content/module-3/theory/module-3_state-actions-reducers.md +445 -0
  93. package/dist/content/module-4/task/module-4_home-task_components.md +187 -0
  94. package/dist/content/module-4/task/module-4_home-task_task.md +139 -0
  95. package/dist/content/module-4/tests/App.test.tsx +54 -0
  96. package/dist/content/module-4/tests/Courses.test.tsx +87 -0
  97. package/dist/content/module-4/tests/CreateCourse.test.tsx +122 -0
  98. package/dist/content/module-4/tests/Login.test.tsx +73 -0
  99. package/dist/content/module-4/theory/module-4_async-redux.md +99 -0
  100. package/dist/content/module-4/theory/module-4_private-routes.md +55 -0
  101. package/dist/content/module-5/task/module-5_home-task_instruction.md +68 -0
  102. package/dist/content/module-5/task/module-5_home-task_task.md +154 -0
  103. package/dist/content/module-5/tests/App.test.tsx +54 -0
  104. package/dist/content/module-5/tests/CourseCard.test.tsx +64 -0
  105. package/dist/content/module-5/tests/Courses.test.tsx +87 -0
  106. package/dist/content/module-5/tests/Header.test.tsx +23 -0
  107. package/dist/content/module-5/theory/module-5_react-testing-library_example.md +379 -0
  108. package/dist/content/module-5/theory/module-5_redux-writing-tests.md +246 -0
  109. package/dist/content-loader.d.ts +7 -0
  110. package/dist/content-loader.js +26 -0
  111. package/dist/content-loader.js.map +1 -0
  112. package/dist/context-builder.d.ts +2 -0
  113. package/dist/context-builder.js +116 -0
  114. package/dist/context-builder.js.map +1 -0
  115. 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 ![bookshelf](./images/bookshelf.png) 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
+ ![](./images/reduxasyncdataflowdiagram-d97ff38a0f4da0f327163170ccc13e80.gif)
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? &#128512;
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
+