Skip to content

Commit e3ca927

Browse files
committed
Add lesson 32
1 parent b778b48 commit e3ca927

15 files changed

+643
-1
lines changed

lessons/lesson32/code/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "react-typescript",
3+
"version": "1.0.0",
4+
"description": "React and TypeScript example starter project",
5+
"keywords": [
6+
"typescript",
7+
"react",
8+
"starter"
9+
],
10+
"main": "src/index.tsx",
11+
"dependencies": {
12+
"loader-utils": "3.2.1",
13+
"react": "18.2.0",
14+
"react-dom": "18.2.0",
15+
"react-scripts": "5.0.1",
16+
"@testing-library/react": "16.3.0",
17+
"@testing-library/dom": "10.4.0"
18+
},
19+
"devDependencies": {
20+
"@types/react": "18.2.38",
21+
"@types/react-dom": "18.2.15",
22+
"typescript": "4.4.4"
23+
},
24+
"scripts": {
25+
"start": "react-scripts start",
26+
"build": "react-scripts build",
27+
"test": "react-scripts test --env=jsdom",
28+
"eject": "react-scripts eject"
29+
},
30+
"browserslist": [
31+
">0.2%",
32+
"not dead",
33+
"not ie <= 11",
34+
"not op_mini all"
35+
]
36+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1, shrink-to-fit=no"
8+
/>
9+
<meta name="theme-color" content="#000000" />
10+
<!--
11+
manifest.json provides metadata used when your web app is added to the
12+
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
13+
-->
14+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
15+
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
16+
<!--
17+
Notice the use of %PUBLIC_URL% in the tags above.
18+
It will be replaced with the URL of the `public` folder during the build.
19+
Only files inside the `public` folder can be referenced from the HTML.
20+
21+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
22+
work correctly both with client-side routing and a non-root public URL.
23+
Learn how to configure a non-root public URL by running `npm run build`.
24+
-->
25+
<title>React App</title>
26+
</head>
27+
28+
<body>
29+
<noscript> You need to enable JavaScript to run this app. </noscript>
30+
<div id="root"></div>
31+
<!--
32+
This HTML file is a template.
33+
If you open it directly in the browser, you will see an empty page.
34+
35+
You can add webfonts, meta tags, or analytics to this file.
36+
The build step will place the bundled scripts into the <body> tag.
37+
38+
To begin the development, run `npm start` or `yarn start`.
39+
To create a production bundle, use `npm run build` or `yarn build`.
40+
-->
41+
</body>
42+
</html>

lessons/lesson32/code/src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import "./styles.css";
2+
3+
export default function App() {
4+
return (
5+
<div className="App">
6+
<h1>Hello CodeSandbox</h1>
7+
<h2>Start editing to see some magic happen!</h2>
8+
</div>
9+
);
10+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { renderHook, act } from "@testing-library/react";
2+
import { useDebounce } from "./useDebounce";
3+
4+
// Используем фейковые таймеры Jest для управления временем в тестах
5+
jest.useFakeTimers();
6+
7+
describe("useDebounce", () => {
8+
it("должен возвращать начальное значение немедленно", () => {
9+
const { result } = renderHook(() => useDebounce("initial", 500));
10+
expect(result.current).toBe("initial");
11+
});
12+
13+
it("не должен обновлять значение до истечения задержки", () => {
14+
const { result, rerender } = renderHook(
15+
({ value, delay }) => useDebounce(value, delay),
16+
{
17+
initialProps: { value: "a", delay: 500 },
18+
}
19+
);
20+
21+
rerender({ value: "b", delay: 500 });
22+
23+
expect(result.current).toBe("a");
24+
25+
act(() => {
26+
jest.advanceTimersByTime(499);
27+
});
28+
29+
expect(result.current).toBe("a");
30+
});
31+
32+
it("должен обновить значение после истечения задержки", () => {
33+
const { result, rerender } = renderHook(
34+
({ value, delay }) => useDebounce(value, delay),
35+
{
36+
initialProps: { value: "a", delay: 500 },
37+
}
38+
);
39+
40+
rerender({ value: "b", delay: 500 });
41+
42+
act(() => {
43+
jest.advanceTimersByTime(500);
44+
});
45+
46+
expect(result.current).toBe("b");
47+
});
48+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useState, useEffect } from "react";
2+
3+
/**
4+
* Хук, который возвращает значение с задержкой.
5+
* @param {*} value - Значение, которое нужно "отложить".
6+
* @param {number} delay - Задержка в миллисекундах.
7+
* @returns {*} - "Отложенное" значение.
8+
*/
9+
export function useDebounce(value, delay) {
10+
// TODO: Реализуйте хук.
11+
// 1. Создайте состояние `debouncedValue` для хранения "отложенного" значения.
12+
// 2. Используйте `useEffect` для установки таймера (`setTimeout`).
13+
// - Эффект должен срабатывать, когда `value` или `delay` изменяются.
14+
// - Внутри таймера, по истечении `delay`, обновите `debouncedValue` на текущее `value`.
15+
// 3. Не забудьте про функцию очистки в `useEffect`! Она должна отменять предыдущий
16+
// таймер (`clearTimeout`), если `value` изменилось до того, как таймер сработал.
17+
// 4. Верните `debouncedValue`.
18+
19+
// Заглушка
20+
return value;
21+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { useDocumentTitle } from "./useDocumentTitle";
3+
4+
describe("useDocumentTitle", () => {
5+
const originalTitle = document.title;
6+
7+
afterEach(() => {
8+
document.title = originalTitle;
9+
});
10+
11+
it("должен устанавливать заголовок документа", () => {
12+
renderHook(() => useDocumentTitle("Новый заголовок"));
13+
expect(document.title).toBe("Новый заголовок");
14+
});
15+
16+
it("должен обновлять заголовок документа при изменении", () => {
17+
const { rerender } = renderHook(({ title }) => useDocumentTitle(title), {
18+
initialProps: { title: "Первый заголовок" },
19+
});
20+
21+
expect(document.title).toBe("Первый заголовок");
22+
23+
rerender({ title: "Второй заголовок" });
24+
expect(document.title).toBe("Второй заголовок");
25+
});
26+
27+
it("должен (опционально) восстанавливать исходный заголовок при размонтировании", () => {
28+
const { unmount } = renderHook(() =>
29+
useDocumentTitle("Временный заголовок")
30+
);
31+
32+
expect(document.title).toBe("Временный заголовок");
33+
34+
unmount();
35+
36+
expect(document.title).toBe(originalTitle);
37+
});
38+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useEffect } from "react";
2+
3+
/**
4+
* Хук для динамического изменения заголовка документа (вкладки браузера).
5+
* @param {string} title - Новый заголовок.
6+
*/
7+
export function useDocumentTitle(title) {
8+
// TODO: Реализуйте хук.
9+
// 1. Используйте `useEffect` для обновления `document.title`.
10+
// - Эффект должен применяться каждый раз, когда изменяется `title`.
11+
// 2. (Опционально, для лучшей практики) Добавьте функцию очистки,
12+
// которая будет возвращать исходный заголовок документа,
13+
// когда компонент, использующий хук, размонтируется.
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { renderHook, act } from "@testing-library/react";
2+
import { useInput } from "./useInput";
3+
4+
describe("useInput", () => {
5+
it("должен устанавливать начальное значение", () => {
6+
const { result } = renderHook(() => useInput("начальное значение"));
7+
expect(result.current.value).toBe("начальное значение");
8+
});
9+
10+
it("должен обновлять значение при вызове onChange", () => {
11+
const { result } = renderHook(() => useInput(""));
12+
13+
// act() гарантирует, что все обновления состояния будут обработаны
14+
act(() => {
15+
result.current.onChange({ target: { value: "новое значение" } });
16+
});
17+
18+
expect(result.current.value).toBe("новое значение");
19+
});
20+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useState } from "react";
2+
3+
/**
4+
* @param {string} initialValue - Начальное значение поля.
5+
* @returns {{value: string, onChange: function}} - Объект со значением и обработчиком.
6+
*/
7+
export function useInput(initialValue = "") {
8+
// TODO: Реализуйте хук.
9+
// 1. Создайте состояние для хранения значения поля с помощью useState.
10+
// 2. Создайте функцию-обработчик onChange, которая будет обновлять это состояние, получая значение из event.target.value.
11+
// 3. Верните объект, содержащий текущее значение (value) и обработчик (onChange).
12+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { renderHook, act } from "@testing-library/react";
2+
import { useLocalStorage } from "./useLocalStorage";
3+
4+
const TEST_KEY = "test-key";
5+
6+
beforeEach(() => {
7+
window.localStorage.clear();
8+
});
9+
10+
describe("useLocalStorage", () => {
11+
it("должен возвращать initialValue, если в localStorage пусто", () => {
12+
const { result } = renderHook(() => useLocalStorage(TEST_KEY, "default"));
13+
expect(result.current[0]).toBe("default");
14+
});
15+
16+
it("должен читать существующее значение из localStorage", () => {
17+
window.localStorage.setItem(TEST_KEY, JSON.stringify("stored value"));
18+
19+
const { result } = renderHook(() => useLocalStorage(TEST_KEY, "default"));
20+
expect(result.current[0]).toBe("stored value");
21+
});
22+
23+
it("должен обновлять значение и записывать его в localStorage", () => {
24+
const { result } = renderHook(() => useLocalStorage(TEST_KEY, ""));
25+
26+
act(() => {
27+
const setValue = result.current[1];
28+
setValue("new value");
29+
});
30+
31+
expect(result.current[0]).toBe("new value");
32+
33+
const storedValue = window.localStorage.getItem(TEST_KEY);
34+
expect(JSON.parse(storedValue)).toBe("new value");
35+
});
36+
});

0 commit comments

Comments
 (0)