diff --git a/lessons/lesson25/code/eventEmitter/.codesandbox/workspace.json b/lessons/lesson25/code/eventEmitter/.codesandbox/workspace.json
new file mode 100644
index 0000000..1870c7a
--- /dev/null
+++ b/lessons/lesson25/code/eventEmitter/.codesandbox/workspace.json
@@ -0,0 +1,8 @@
+{
+ "responsive-preview": {
+ "Mobile": [320, 675],
+ "Tablet": [1024, 765],
+ "Desktop": [1400, 800],
+ "Desktop HD": [1920, 1080]
+ }
+}
diff --git a/lessons/lesson25/code/eventEmitter/index.html b/lessons/lesson25/code/eventEmitter/index.html
new file mode 100644
index 0000000..9f6c78c
--- /dev/null
+++ b/lessons/lesson25/code/eventEmitter/index.html
@@ -0,0 +1,13 @@
+
+
+ Parcel Sandbox
+
+
+
+
+ Event Emitter / Event Bus
+
+
+
+
+
diff --git a/lessons/lesson25/code/eventEmitter/package.json b/lessons/lesson25/code/eventEmitter/package.json
new file mode 100644
index 0000000..b82ef30
--- /dev/null
+++ b/lessons/lesson25/code/eventEmitter/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "event-emitter",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.html",
+ "scripts": {
+ "start": "parcel index.html --open",
+ "build": "parcel build index.html"
+ },
+ "dependencies": {
+ "@types/jest": "26.0.21",
+ "parcel-bundler": "^1.6.1"
+ },
+ "devDependencies": {
+ "typescript": "4.2.3"
+ },
+ "resolutions": {
+ "@babel/preset-env": "7.13.8"
+ },
+ "keywords": []
+}
diff --git a/lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts b/lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts
new file mode 100644
index 0000000..92618f8
--- /dev/null
+++ b/lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts
@@ -0,0 +1,80 @@
+import { EventEmitter } from "./EventEmitter";
+
+describe("EventEmitter", () => {
+ describe("formal public interface", () => {
+ it("is a constructor", () => {
+ expect(typeof EventEmitter).toBe("function");
+ expect(new EventEmitter() instanceof EventEmitter).toBe(true);
+ });
+
+ it("has public methods", () => {
+ const eventEmitter = new EventEmitter();
+
+ expect(typeof eventEmitter.on).toBe("function");
+ expect(typeof eventEmitter.off).toBe("function");
+ expect(typeof eventEmitter.trigger).toBe("function");
+ });
+ });
+
+ describe("runtime logic", () => {
+ let eventEmitter: EventEmitter;
+ const eventName = "eventName";
+ const eventPayload = { name: "Bob" };
+
+ beforeEach(() => {
+ eventEmitter = new EventEmitter();
+ });
+ it("allows to listen to the events", () => {
+ const spy = jest.fn();
+ eventEmitter.on(eventName, spy);
+ expect(spy).not.toHaveBeenCalled();
+ eventEmitter.trigger(eventName, eventPayload);
+ expect(spy).toHaveBeenCalledWith(eventPayload);
+ });
+
+ it("supports multiple listeners for the event", () => {
+ const spy1 = jest.fn();
+ const spy2 = jest.fn();
+ eventEmitter.on(eventName, spy1);
+ eventEmitter.on(eventName, spy2);
+ expect(spy1).not.toHaveBeenCalled();
+ expect(spy2).not.toHaveBeenCalled();
+ eventEmitter.trigger(eventName, eventPayload);
+ expect(spy1).toHaveBeenCalledWith(eventPayload);
+ expect(spy2).toHaveBeenCalledWith(eventPayload);
+ });
+
+ it("allows to unsubscribe from the events", () => {
+ const spy1 = jest.fn();
+ const spy2 = jest.fn();
+ eventEmitter.on(eventName, spy1);
+ eventEmitter.on(eventName, spy2);
+ eventEmitter.off(eventName, spy1);
+ eventEmitter.trigger(eventName, eventPayload);
+ expect(spy1).not.toHaveBeenCalled();
+ expect(spy2).toHaveBeenCalledWith(eventPayload);
+ });
+
+ describe("edge cases", () => {
+ it("handles events with no listeners", () => {
+ expect(() => {
+ eventEmitter.trigger(eventName, eventPayload);
+ }).not.toThrowError();
+ });
+
+ it("handles invalid unsubscriptions", () => {
+ eventEmitter.on("x", jest.fn());
+ expect(() => {
+ eventEmitter.off("x", () => {});
+ eventEmitter.off(eventName, () => {});
+ }).not.toThrowError();
+ });
+
+ it("handles triggers with no subscriptions", () => {
+ expect(() => {
+ eventEmitter.trigger(eventName, eventPayload);
+ }).not.toThrowError();
+ });
+ });
+ });
+});
diff --git a/lessons/lesson25/code/eventEmitter/src/EventEmitter.ts b/lessons/lesson25/code/eventEmitter/src/EventEmitter.ts
new file mode 100644
index 0000000..c573ed4
--- /dev/null
+++ b/lessons/lesson25/code/eventEmitter/src/EventEmitter.ts
@@ -0,0 +1,3 @@
+export class EventEmitter {
+ // @todo: put your code here
+}
diff --git a/lessons/lesson25/code/eventEmitter/src/index.ts b/lessons/lesson25/code/eventEmitter/src/index.ts
new file mode 100644
index 0000000..f0727cd
--- /dev/null
+++ b/lessons/lesson25/code/eventEmitter/src/index.ts
@@ -0,0 +1,21 @@
+// import { EventEmitter } from "./EventEmitter";
+
+// (document.querySelector("#app") as HTMLElement).innerHTML = `
+//
+//
+//
+// `;
+// const input1 = document.querySelector("input[name=input1") as HTMLInputElement;
+// const input2 = document.querySelector("input[name=input2") as HTMLInputElement;
+// const header = document.querySelector("h1") as HTMLHeadingElement;
+
+// const eventEmitter = new EventEmitter();
+
+// eventEmitter.on("changeText", (text) => (header.innerHTML = text));
+
+// input1.addEventListener("keypress", (ev) =>
+// eventEmitter.trigger("changeText", (ev.target as HTMLInputElement).value)
+// );
+// input2.addEventListener("keypress", (ev) =>
+// eventEmitter.trigger("changeText", (ev.target as HTMLInputElement).value)
+// );
diff --git a/lessons/lesson25/code/eventEmitter/tsconfig.json b/lessons/lesson25/code/eventEmitter/tsconfig.json
new file mode 100644
index 0000000..587af1c
--- /dev/null
+++ b/lessons/lesson25/code/eventEmitter/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "module": "commonjs",
+ "jsx": "preserve",
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "allowJs": true,
+ "lib": ["es6", "dom"],
+ "rootDir": "src",
+ "moduleResolution": "node"
+ }
+}
diff --git a/lessons/lesson25/images/EventBus.jpeg b/lessons/lesson25/images/EventBus.jpeg
new file mode 100644
index 0000000..7948f81
Binary files /dev/null and b/lessons/lesson25/images/EventBus.jpeg differ
diff --git a/lessons/lesson25/images/ObservableUML.png b/lessons/lesson25/images/ObservableUML.png
new file mode 100644
index 0000000..44f574a
Binary files /dev/null and b/lessons/lesson25/images/ObservableUML.png differ
diff --git a/lessons/lesson25/lesson.md b/lessons/lesson25/lesson.md
index 6fb9e08..d81329c 100644
--- a/lessons/lesson25/lesson.md
+++ b/lessons/lesson25/lesson.md
@@ -1 +1,225 @@
-# Lesson 25
+---
+title: Занятие 25
+description: Связь модулей - от интерфейсов до EventBus
+---
+
+# OTUS
+
+## Javascript Basic
+
+
+
+## Вопросы?
+
+
+
+## Связь модулей - от интерфейсов до EventBus
+
+
+
+### Разберемся с задачей
+
+
+
+Для начала два термина - **связность(_cohesion_)** и **связанность(_coupling_)**.
+
+[Связность]() - на сколько составные части направлены на решение одной задачи.
+
+[Связанность]() - на сколько одни модули зависят от других (и как много они знают друг о друге)
+
+
+
+[Качественный дизайн обладает слабой связанностью (low coupling) и сильной связностью (high cohesion).](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9)
+
+Это значит, что программный компонент имеет небольшое число внешних связей и отвечает за решение близких по смыслу задач.
+
+
+
+**Слабое зацепление (Low Coupling)** и **Высокая связность (High Cohesion)** это 2 из 9 [**шаблонов GRASP**]()
+
+
+
+Высокая связность говорит об эффективности программы (или ее отдельных модулей).
+
+Низкая связанность означает легкость рефакторинга и переиспользуемость кода.
+
+
+
+### Вопросы?
+
+
+
+### Наблюдатель (Observer)
+
+
+
+[Наблюдатель](https://refactoring.guru/ru/design-patterns/observer) - подход (паттерн), позволяющий одним объектам следить и реагировать на события, происходящие в других объектах.
+
+
+
+
+
+
+
+На самом деле вы с ним уже работали - это [EventTarget](https://developer.mozilla.org/ru/docs/Web/API/EventTarget)
+
+
+
+Данный шаблон часто применяют в ситуациях, в которых отправителя сообщений не интересует, что делают получатели с предоставленной им информацией.
+
+
+
+Может быть представлен как
+
+
+
+```ts
+IObservable {
+ addObserver(event, handler)
+ removeObserver(event, handler)
+ notifyObserver(event, data)
+}
+```
+
+
+
+или
+
+
+
+```ts
+EventTarget {
+ addEventListener(event, handler)
+ removeEventListener(event, handler)
+ dispatchEvent(event)
+}
+```
+
+
+
+или
+
+
+
+```ts
+Backbone.Events {
+ on(event, handler)
+ off(event, handler)
+ trigger(event)
+}
+```
+
+
+
+Иногда могут добавлять вспомогательные методы, например
+
+
+
+```ts
+Backbone.Events {
+ // ...
+ once(event, handler)
+}
+```
+
+
+
+```ts
+document.querySelector(element).addEventListener("click", (ev) => {
+ alert("Boom!");
+});
+```
+
+
+
+Оговорка: чаще всего обработчиком события является функция. Но это также может быть и объект ([EventListener](https://developer.mozilla.org/ru/docs/Web/API/EventListener)) - в зависимости от реализации.
+
+
+
+### Вопросы?
+
+
+
+### Посредник (Mediator)
+
+
+
+[Посредник](https://refactoring.guru/ru/design-patterns/mediator) - это поведенческий паттерн проектирования, который позволяет уменьшить связанность множества классов между собой, благодаря перемещению этих связей в один класс-посредник.
+
+
+
+**Задача:** Обеспечить взаимодействие множества объектов, сформировав при этом слабую связанность и избавив объекты от необходимости явно ссылаться друг на друга.
+
+**Решение:** Создать объект, инкапсулирующий способ взаимодействия множества объектов.
+
+**Преимущества:** Устраняется связанность между "Коллегами", централизуется управление.
+
+
+
+Самый распространенный (и простой) вариант реализации паттерна - с использованием **EventEmitter** интерфейса (**Event Bus** - Шина событий).
+
+
+
+Разница, по сравнению с обычным использованием EventTarget:
+
+- события в EventTarget генерирует сам объект, при работе с EventBus это делают сторонние объекты
+- список событий при работе с EventTarget ограничен устройством объекта, при работе с EventBus он определяется участниками
+
+
+
+
+
+
+
+При этом, чтобы избежать коллизии имен событий, зачастую вводят `namespaces`, в формате **{NAMESPACE}:{EVENT NAME}**. Например `user:add`, `searchHistory:add`.
+
+Нужно отметить, что по-хорошему, префиксы делаются на основе сущностей, а не на основе модулей (иначе происходит раскрытие структуры системы).
+
+
+
+```ts
+const eventBus = new EventBus();
+
+// module 1
+eventBus.on("city:changed", (cityName) => console.log(`New city: ${cityName}`));
+
+// module 2
+eventBus.trigger("city:changed", "Minsk");
+```
+
+
+
+Как мы могли бы применить это к уже сделанным домашним заданиям?
+
+
+
+### Вопросы?
+
+
+
+### Практика
+
+
+
+[Реализовать Event Emitter](https://codesandbox.io/s/github/vvscode/otus--javascript-basic/tree/master/lessons/lesson33/code/eventEmitter)
+
+
+
+Реализовать поверх существующего функционала метод **once** (для одноразового вызова обработчика).
+
+
+
+### Вопросы?
+
+
+
+Дополнительные материалы:
+
+- [Backbone Events](https://backbonejs.org/#Events) и [исходники](https://backbonejs.org/docs/backbone.html#section-17)
+- [EventTarget simple implementation](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
+- [Паттерны проектирования понятным языком](https://refactoring.guru/ru/design-patterns)
+- [Design patterns for humans!](https://github.com/sohamkamani/javascript-design-patterns-for-humans)
+
+
+
+### Опрос о занятии
diff --git a/lessons/lesson28/code/routing-examples/README.md b/lessons/lesson28/code/routing-examples/README.md
new file mode 100644
index 0000000..000d747
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/README.md
@@ -0,0 +1,3 @@
+# vanilla-client-side-routing-examples
+
+Created with CodeSandbox
diff --git a/lessons/lesson28/code/routing-examples/examples/hash-api.js b/lessons/lesson28/code/routing-examples/examples/hash-api.js
new file mode 100644
index 0000000..939ad80
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/examples/hash-api.js
@@ -0,0 +1,28 @@
+/* eslint-disable */
+
+const render = () => {
+ const route = location.hash.replace("#", "") || "/";
+ document.getElementById("root").innerHTML = `"${route}" page
`;
+};
+
+// 1. Handle initial page load
+window.addEventListener("load", () => {
+ render(); // 👈
+});
+
+// 2. Handle hash changes
+window.addEventListener("hashchange", () => {
+ render(); // 👈
+});
+
+// 3. Catch tag clicks
+document.body.addEventListener("click", (event) => {
+ if (!event.target.matches("a")) {
+ return;
+ }
+ event.preventDefault();
+ const url = event.target.getAttribute("href");
+ location.hash = url; // doesn't reload page
+ // location.href = url; // reloads page
+ // location.replace(url); // reloads page
+});
diff --git a/lessons/lesson28/code/routing-examples/examples/history-api.js b/lessons/lesson28/code/routing-examples/examples/history-api.js
new file mode 100644
index 0000000..7be0621
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/examples/history-api.js
@@ -0,0 +1,28 @@
+/* eslint-disable */
+
+const render = () => {
+ const route = location.pathname;
+ document.getElementById("root").innerHTML = `"${route}" page
`;
+};
+
+// 1. Handle initial page load
+window.addEventListener("load", () => {
+ render(); // 👈
+});
+
+// 2. Handle history navigations. alternative "window.onpopstate"
+window.addEventListener("popstate", (event) => {
+ render();
+});
+
+// 3. Catch tag clicks + trigger change handler
+document.body.addEventListener("click", (event) => {
+ if (!event.target.matches("a")) {
+ return;
+ }
+ event.preventDefault();
+ let url = event.target.getAttribute("href");
+ history.pushState({ foo: "bar" }, document.title, url);
+ // history.replaceState({ foo: "bar" }, url, url);
+ render(); // 👈
+});
diff --git a/lessons/lesson28/code/routing-examples/examples/practice.js b/lessons/lesson28/code/routing-examples/examples/practice.js
new file mode 100644
index 0000000..a7ebba4
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/examples/practice.js
@@ -0,0 +1,44 @@
+/* eslint-disable */
+
+/**
+ * TODO: modify router.js to support
+ * 1. unsubscribe function.
+ * Hint: inside Router.go function return unsubscribe function,
+ * which will remove listener by id
+ * 2. onLeave callback
+ * Hint: Add 3rd 'onLeave' parameter to Router.on + save in listener object
+ * Check in Router.handleListener if previousPath matches listener
+ */
+
+const render = (content) =>
+ (document.getElementById("root").innerHTML = `${content}
`);
+
+const createLogger =
+ (content, shouldRender = true) =>
+ (...args) => {
+ console.log(`LOGGER: ${content} args=${JSON.stringify(args)}`);
+ if (shouldRender) {
+ render(content);
+ }
+ };
+
+const router = Router();
+
+const unsubscribe = router.on(/.*/, createLogger("/.*"));
+router.on(
+ (path) => path === "/contacts",
+ createLogger("/contacts"), // onEnter
+ createLogger("[leaving] /contacts", false) // onLeave
+);
+router.on("/about", createLogger("/about"));
+router.on("/about/us", createLogger("/about/us"));
+
+document.body.addEventListener("click", (event) => {
+ if (!event.target.matches("a")) {
+ return;
+ }
+ event.preventDefault();
+ let url = event.target.getAttribute("href");
+ router.go(url);
+ unsubscribe();
+});
diff --git a/lessons/lesson28/code/routing-examples/examples/router.js b/lessons/lesson28/code/routing-examples/examples/router.js
new file mode 100644
index 0000000..edd14ea
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/examples/router.js
@@ -0,0 +1,92 @@
+/* eslint-disable */
+
+// IMPLEMENTATION
+function Router() {
+ let listeners = [];
+ let currentPath = location.pathname;
+ let previousPath = null;
+
+ const isMatch = (match, path) =>
+ (match instanceof RegExp && match.test(path)) ||
+ (typeof match === "function" && match(path)) ||
+ (typeof match === "string" && match === path);
+
+ const handleListener = ({ match, onEnter, onLeave }) => {
+ const args = { currentPath, previousPath, state: history.state };
+
+ isMatch(match, currentPath) && onEnter(args);
+ onLeave && isMatch(match, previousPath) && onLeave(args);
+ };
+
+ const handleAllListeners = () => listeners.forEach(handleListener);
+
+ const generateId = () => {
+ const getRandomNumber = () =>
+ Math.floor(Math.random() * listeners.length * 1000);
+ const doesExist = (id) => listeners.find((listener) => listener.id === id);
+
+ let id = getRandomNumber();
+ while (doesExist(id)) {
+ id = getRandomNumber();
+ }
+ return id;
+ };
+
+ const on = (match, onEnter, onLeave) => {
+ const id = generateId();
+
+ const listener = { id, match, onEnter, onLeave };
+ listeners.push(listener);
+ handleListener(listener);
+
+ return () => {
+ listeners = listeners.filter((listeners) => listeners.id !== id);
+ };
+ };
+
+ const go = (url, state) => {
+ previousPath = currentPath;
+ history.pushState(state, url, url);
+ currentPath = location.pathname;
+
+ handleAllListeners();
+ };
+
+ window.addEventListener("popstate", handleAllListeners);
+
+ return { on, go };
+}
+
+// USAGE
+const render = (content) =>
+ (document.getElementById("root").innerHTML = `${content}
`);
+
+const createLogger =
+ (content, shouldRender = true) =>
+ (...args) => {
+ console.log(`LOGGER: ${content} args=${JSON.stringify(args)}`);
+ if (shouldRender) {
+ render(content);
+ }
+ };
+
+const router = Router();
+
+const unsubscribe = router.on(/.*/, createLogger("/.*"));
+router.on(
+ (path) => path === "/contacts",
+ createLogger("/contacts"), // onEnter
+ createLogger("[leaving] /contacts", false) // onLeave
+);
+router.on("/about", createLogger("/about"));
+router.on("/about/us", createLogger("/about/us"));
+
+document.body.addEventListener("click", (event) => {
+ if (!event.target.matches("a")) {
+ return;
+ }
+ event.preventDefault();
+ let url = event.target.getAttribute("href");
+ router.go(url);
+ unsubscribe();
+});
diff --git a/lessons/lesson28/code/routing-examples/index.html b/lessons/lesson28/code/routing-examples/index.html
new file mode 100644
index 0000000..06129c6
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Client-Side URL change examples
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lessons/lesson28/code/routing-examples/package.json b/lessons/lesson28/code/routing-examples/package.json
new file mode 100644
index 0000000..eb165e0
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "vanilla-client-side-routing-examples",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.html",
+ "scripts": {
+ "start": "serve",
+ "build": "echo This is a static template, there is no bundler or bundling involved!"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/codesandbox-app/static-template.git"
+ },
+ "keywords": [],
+ "author": "Ives van Hoorne",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/codesandbox-app/static-template/issues"
+ },
+ "homepage": "https://github.com/codesandbox-app/static-template#readme",
+ "devDependencies": {
+ "serve": "^11.2.0"
+ }
+}
diff --git a/lessons/lesson28/code/routing-examples/sandbox.config.json b/lessons/lesson28/code/routing-examples/sandbox.config.json
new file mode 100644
index 0000000..5866ed7
--- /dev/null
+++ b/lessons/lesson28/code/routing-examples/sandbox.config.json
@@ -0,0 +1,3 @@
+{
+ "template": "static"
+}
diff --git a/lessons/lesson28/lecture.md b/lessons/lesson28/lecture.md
new file mode 100644
index 0000000..52591c9
--- /dev/null
+++ b/lessons/lesson28/lecture.md
@@ -0,0 +1,372 @@
+---
+title: Занятие 28
+description: Использование клиентского роутинга для создания одностраничных приложений. Деплой одностраничных приложений
+---
+
+# OTUS
+
+## Javascript Basic
+
+
+
+## Клиентский роутинг
+
+Как строится одностраничное приложение (SPA)
+
+
+
+### Проверка
+
+- Хорошо ли видно и слышно?
+- Проверить идёт ли запись
+
+
+
+## Клиентский роутинг
+
+Как строится одностраничное приложение (SPA)
+
+
+
+### Цели занятия
+
+- Разобраться какие API можно использовать для организации SPA
+
+- Научиться создавать клиентский роутинг
+
+
+
+### Маршрут вебинара
+
+- Введение
+- Hash API
+- History API
+- Router
+- Практика
+- Итоги
+
+
+
+## Введение
+
+
+
+**Что такое URL**
+
+
+
+> Единый указатель ресурса (англ. [**Uniform Resource Locator**](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL), URL) — единообразный локатор (определитель местонахождения) ресурса.
+
+> Ранее назывался Universal Resource Locator — универсальный указатель ресурса. URL служит стандартизированным способом записи **адреса ресурса в сети Интернет**.
+
+
+
+```
+scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
+```
+
+```
+https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL#anchor
+```
+
+
+
+[**Что происходит, когда вы вводите URL в браузере**](http://wsvincent.com/what-happens-when-url/)
+
+
+
+**Что такое клиентский роутинг**
+
+
+
+> Клиентский роутинг (**client-side routing**) это, когда пользователь перемещается по приложению/веб-сайту, и при этом **не происходит полной перезагрузки страницы**, даже если URL-адрес страницы изменяется. Вместо этого **используется JavaScript для обновления URL-адреса**, а также для извлечения и отображения нового содержимого.
+
+
+
+**Способы управления URL на клиенте**
+
+
+
+- Hash API
+- History API
+
+
+
+## `Hash API`
+
+
+
+\*Старый способ. До появления HTML5.
+
+
+
+> Способ управления состояниям фрагмента URL
+
+```
+scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
+```
+
+
+
+- window.location.hash
+- `window.onhashchange` / `"hashchange"` event
+
+
+
+[Пример](https://codesandbox.io/s/vigorous-black-vzbit?file=/index.html)
+
+```js
+document.body.addEventListener("click", (event) => {
+ if (!event.target.matches("a")) {
+ return;
+ }
+ event.preventDefault();
+ let url = event.target.getAttribute("href");
+ location.hash = url; // <= set only hash or URL
+});
+
+window.addEventListener("hashchange", () => {
+ // <= handle/catch hash changes
+ console.log(`hashchange: ${location.hash}`);
+});
+```
+
+
+
+## `History API`
+
+
+
+[Новое API](https://caniuse.com/?search=history) HTML5
+
+
+
+> [**History API**](https://developer.mozilla.org/en-US/docs/Web/API/History_API) опирается на один DOM интерфейс — объект **History**
+
+> Каждая вкладка браузера имеет уникальный объект History, который находится в `window.history`
+
+
+
+> [**History**](https://developer.mozilla.org/en-US/docs/Web/API/History) имеет несколько методов, событий и свойств, которыми мы можем **управлять из JavaScript**.
+
+
+
+
+
+```js
+/* Количество записей в текущей сессии истории */
+window.history.length
+
+/* Возвращает текущий объект состояния истории */
+window.history.state
+
+/* Метод, позволяющий гулять по истории.
+ * В качестве аргумента передается смещение, относительно текущей позиции.
+ * Если передан 0, то будет обновлена текущая страница.
+ * Если индекс выходит за пределы истории, то ничего не произойдет. */
+window.history.go(n)
+
+/* Метод, идентичный вызову go(-1) */
+window.history.back()
+
+/* Метод, идентичный вызову go(1) */
+window.history.forward()
+
+/* Добавляет элемент истории */
+window.history.pushState(data, title [, url])
+
+/* Обновляет текущий элемент истории */
+window.history.replaceState(data, title [, url])
+```
+
+
+
+```js
+/* Триггерится при `history.go/back/forward` или при браузерных кликах */
+window.addEventListener("popstate", (event) => console.log(event.state));
+```
+
+
+
+[Пример](https://codesandbox.io/s/vigorous-black-vzbit?file=/index.html)
+
+```js
+document.body.addEventListener("click", (event) => {
+ if (!event.target.matches("a")) {
+ return;
+ }
+ event.preventDefault();
+ let url = event.target.getAttribute("href");
+ history.pushState({}, url, url); // <--
+});
+
+/* Триггерится при `history.go/back/forward` или при браузерных кликах */
+window.addEventListener("popstate", (event) => {
+ console.log(
+ "location: " + document.location + ", state: " + JSON.stringify(event.state)
+ );
+});
+```
+
+
+
+> \*Нужна настройка сервера, т.к. при обновлении / передаче ссылки должна загрузиться начальная страница
+
+
+
+## `Router`
+
+
+
+> Обработчик URL - называется роутером (**Router**)
+
+> Router определяет какой код должен выполняться в зависимости от адреса.
+> Логика `router`'а может быть завязана на параметры.
+
+
+
+\*Бывает серверный и **браузерный** роутинг/роутер.
+
+
+
+\*Router **не встроенные API**, а скорее общепринятый термин.
+
+
+
+Очень много готовых библиотек/статей:
+
+- [Pilot: многофункциональный JavaScript роутер](https://habrahabr.ru/company/mailru/blog/172333/)
+- [Роутер на JavaScript](http://blog.byndyu.ru/2009/09/javascript.html)
+- [A modern JavaScript router in 100 lines](http://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-100-lines-history-api-pushState-hash-url)
+- [A simple minimalistic JavaScript router with a fallback for older browsers.](https://github.com/krasimir/navigo)
+- [router.js](https://github.com/tildeio/router.js/)
+
+
+
+Example. Simple Route Interface
+
+```sh
+# Interface of Route
+IRoute {
+ match # String | RegExp | function
+ onEnter([data]) # function
+}
+```
+
+```js
+// Example
+const route = {
+ match: "/",
+ onEnter: () => console.log("onEnter index"),
+};
+```
+
+
+
+Example. Advanced Route Interface
+
+```sh
+# Interface of Route
+IRoute {
+ match # String | RegExp | function
+ onEnter([data]) # function
+ onLeave([data]) # function
+ onBeforeEnter([data]) # function
+}
+```
+
+```js
+// Example
+const route = {
+ match: "/",
+ onEnter: () => console.log("onEnter index"),
+ onLeave: () => console.log("onLeave index"),
+};
+```
+
+
+
+Example. Router Interface 1
+
+```sh
+# Interface of Route
+IRouter {
+ add(route) # function
+ remove(route) # function
+ go(url, [param]) # function
+}
+```
+
+
+
+Example. Router Interface 2
+
+```sh
+# Interface of Route
+IRouter {
+ on(match, onEnter) # function
+ go(url, [params]) # function
+}
+```
+
+
+
+[Пример](https://codesandbox.io/s/vigorous-black-vzbit?file=/index.html)
+
+
+
+## Практика
+
+
+
+To-do:
+
+1. Fork sandbox
+1. Implement unsubscribe/remove functionality
+1. Add support for "onLeave" callback
+
+```sh
+# Interface of Route
+IRouter {
+ on(match, onEnter, [onLeave]) # function -> function
+ go(url, [params]) # function
+}
+```
+
+
+
+[codesandbox](https://codesandbox.io/s/vigorous-black-vzbit?file=/examples/practice.js)
+
+
+
+## Итоги
+
+> 1. **Клиентский роутинг** - навигацию по приложению/веб-сайту без **перезагрузки страницы**
+
+> 2. Способы управления URL на клиенте:
+> (old) **Hash API** и (new) **History API**
+
+> 3. **Router** (термин)- обработчик URL, определяет какой код должен выполняться в зависимости от адреса. **\*Не встроённое API**
+
+
+
+
+
+### Деплой одностраничных приложений
+
+- [devServer.historyApiFallback](https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback)
+- [output.publicPath](https://webpack.js.org/configuration/output/#outputpublicpath)
+- [WebpackHTMLPlugin filename option](https://github.com/jantimon/html-webpack-plugin#options)
+- [404 page for Github Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-custom-404-page-for-your-github-pages-site)
+- [Пример настройки webpack для работы с history API](https://github.com/vvscode/webpack-gh-pages/pulls)
+- [Пример работы роутера с параметризацией](https://gzh7s.csb.app/#city=Moscow)
+
+
+
+### Вопросы?
+
+
+
+### Ссылки
+
+- [Understanding client side routing by implementing a router in Vanilla JS](https://www.willtaylor.blog/client-side-routing-in-vanilla-js/#:~:text=What%20is%20client%20side%20routing,fetch%20and%20display%20new%20content.)
diff --git a/lessons/lesson29/lesson.md b/lessons/lesson29/lesson.md
index e9a408e..b0bc87f 100644
--- a/lessons/lesson29/lesson.md
+++ b/lessons/lesson29/lesson.md
@@ -1 +1,246 @@
-# Lesson 29
+---
+title: Занятие 29
+description: Различие между стандартами языка, инструменты транспиляции, проблемы типизации
+---
+
+# OTUS
+
+## Javascript Basic
+
+
+
+### Вопросы?
+
+
+
+## Различие между стандартами языка, инструменты транспиляции, проблемы типизации
+
+
+
+### Различие между стандартами языка, инструменты транспиляции
+
+
+
+Какая текущая версия языка ECMAScript?
+
+
+
+Какие новые удобные вещи появились в сравнении с ES5 ?
+
+
+
+[1](https://medium.com/@dupski/what-major-new-features-were-in-each-javascript-version-what-version-should-i-target-25526c498687), [2](http://es6-features.org/), [3](https://github.com/lukehoban/es6features), [4](https://www.w3schools.com/js/js_versions.asp)
+
+
+
+В каком году появилась версия Javascript следующая за ES5 ?
+
+
+
+Какие инструменты позволяют использовать новые версии, в окружении, которое работает со старыми версиями языка?
+
+
+
+В каком году появился Babel.js?
+
+
+
+
+
+До того, как язык начал развиваться, его пытались модернизировать или заменить другими языками ([которые бы транспилировались в Javascript](https://github.com/jashkenas/coffeescript/wiki/List-of-languages-that-compile-to-JS))
+
+
+
+Примерно там же (в 2012) появился язык [TypeScript](https://ru.wikipedia.org/wiki/TypeScript).
+
+Разработчиком языка TypeScript является Андерс Хейлсберг (англ. Anders Hejlsberg), создавший ранее Turbo Pascal, Delphi и C#.
+
+
+
+Главное чем отличался TS:
+
+- был **надмножеством** Javascript
+- поддерживал классы
+- добавлял работу с типизацией
+
+
+
+### Вопросы?
+
+
+
+### Проблемы типизации
+
+
+
+Виды типизации:
+
+- динамическая / статическая
+- слабая / сильная
+- неявная / явная
+
+Какая типизация в Javascript?
+
+
+
+Большая часть проблем в разработке связанна с незначительными факторами:
+
+- опечатки _(неправильно назвали переменную или свойство)_
+- невнимательность _(не передали параметр, или передали неправильный параметр)_
+- потеря контекста _(не учли контекст вызова)_
+- мелкие упущения _(не учли все места, которые затрагиваются изменениями)_
+
+
+
+Это характерно для всех языков, но особенности типизации дают для этого хорошую почву.
+
+
+
+Изначальная идея TypeScript - добавить явное указание типов в код. Что должно значительно уменьшить число таких проблем (или сделать их обнаружение более быстрым, без запуска кода).
+
+
+
+```ts
+function getCityMap(city) {
+ return `https://map.com?q=${city}`;
+}
+```
+
+Что с этим кодом может пойти не так?
+
+
+
+```ts
+function getCityMap(city: string): string {
+ return `https://map.com?q=${city}`;
+}
+```
+
+
+
+Помимо работы с стандартными типами для Javascript, TS позволял определить тип как класс / интерфейс / форму (shape).
+
+[Например](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgArQM4HsTIN4BQyxyIcAthAFzIZhSgDmA3AQL4EFgCeADigGVycKGHRRsuALz4iJMpRp0GIFnOLAAjjRABXcgCNorDgQQ46yA1gM0hIsZhzIZhEqQrVkAcgBCN7wAadWQtGgB2AA52VjMLMCsbcUkaZOcZawNmIA) описать свойства и их тип, что было полезно для языка, где классами пользоваться еще не привыкли, а вот объекты были в ходу уже давно.
+
+
+
+Со временем функционал расширялся, в нем появлялись новые вещи (вроде декораторов) и обработчик языка становился умнее (например появился вывод типов).
+
+
+
+Какую еще проблему решает Typescript?
+
+
+
+```js
+/**
+ * Returns city map url
+ * @param {string} city
+ * @return {string} url
+ */
+function getCityMap(city) {
+ return `https://map.com?q=${city}`;
+}
+```
+
+Какая проблема с этим кодом?
+
+
+
+Указание типов
+
+- документирует код описанием контрактов
+- позволяет проверять использование кода на соответствие контрактам (и чем больше кода, тем это сложнее и важнее)
+
+
+
+**Важно!** Typescript _(как и другие языки транспилирующиеся в JS)_ **НЕ работает в браузере**.
+
+Т.е. все его проверки работают только в момент написания или сборки кода! _(в браузере будет работать уже javascript, полученный при транспиляции Typescript)_
+
+
+
+### Вопросы?
+
+
+
+### Зачем мы будем работать с Typescript
+
+
+
+1. Экономит нервы и время (поначалу код на typescript может писаться дольше, но со временем эта проблема уходит)
+1. Когда то _был_ еще [flow language](https://flow.org/), но сейчас Typescript самая популярная "альтернатива" js
+1. _"Необходим"_ для больших проектов (и очень рекомендован для маленьких)
+1. Востребован при поиске работы
+1. Улучшает понимание работы кода и понимание некоторых паттернов
+ разработки
+
+
+
+Что мы разберем на курсе:
+
+- как использовать базовые возможности языка (базовые типы, интерфейсы, типизация функций)
+- как настроить окружение для работы
+- как применять некоторые "продвинутые" темы (немного generic, вывод типов,кастинг)
+
+
+
+Базовые темы мы разберем отдельным занятием, остальное будем смотреть по ходу остальных занятий.
+
+
+
+Дальше код будет писаться на Typescript.
+
+
+
+### Вопросы?
+
+
+
+### Установка, настройка и запуск Typescript
+
+
+
+1. Создать новый npm-проект
+1. Установить пакет [`typescript`](https://www.npmjs.com/package/typescript)
+1. Инициализировать конфигурацию для Typescript
+1. Создать файлы
+1. Выполнить команду `npx tsc` (Для проверки, без сборки `npx tsc --noEmit`)
+
+
+
+```bash
+mkdir typescript-test
+cd typescript-test
+
+npm init -y
+
+npm install typescript --save-dev
+
+npx tsc --init
+
+echo "export function x(n: number) { console.log('x with n', n); }" > "x.ts"
+echo "import { x } from './x'; " > "index.ts"
+echo "function y() { x(1); }" >> "index.ts"
+echo "y();" >> "index.ts"
+
+npx tsc
+```
+
+
+
+### Вопросы?
+
+
+
+Дополнительные материалы:
+
+- [The TypeScript Handbook (30 минут для быстрого старта)](https://www.typescriptlang.org/docs/handbook/intro.html)
+- [Основное про типы в TS](https://canonium.com/category/typescript)
+- TypeScript как будущее энтерпрайзного JavaScript [1](https://dou.ua/lenta/articles/typescript-1/) и [2](https://dou.ua/lenta/articles/typescript-2/)
+- [Глубокое погружение в TypeScript](https://github.com/etroynov/typescript-book)
diff --git a/lessons/lesson30/lesson.md b/lessons/lesson30/lesson.md
index c879a05..f71e6b5 100644
--- a/lessons/lesson30/lesson.md
+++ b/lessons/lesson30/lesson.md
@@ -1 +1,824 @@
-# Lesson 30
+---
+title: Занятие 30
+description: Что такое React, JSX, настройка окружения
+---
+
+# OTUS
+
+## Javascript Basic
+
+
+
+Вопросы?
+
+
+
+### [React](https://ru.reactjs.org/)
+
+
+
+#### Проблемы шаблонизации
+
+- скорость разработки приложения
+- скорость работы приложения
+
+
+
+#### React
+
+- декларативный
+- основан на [компонентах](https://reactjs.org/docs/components-and-props.html)
+- научитесь однажды — пишите где угодно
+
+
+
+Особенности **React**
+
+- с компонентами (но не [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components))
+- оптимизация обновлений через [vDOM](https://reactjs.org/docs/faq-internals.html)
+- минимальный синтаксис шаблонов — [JSX](https://facebook.github.io/jsx/)
+
+
+
+Идея скорости — [**Virtual DOM**](https://reactjs.org/docs/faq-internals.html)
+
+- архитектура React — отрисовать минимально необходимые изменения
+- работа с DOM напрямую — медленно
+- vDOM — модель настоящего DOM на легких js-объектах
+
+
+
+
+
+
+
+### React - абстрактная идея
+
+- может использоваться на разных платформах (не только Web, но и нативные системы и что-угодно еще представляющее UI - [React Native](https://reactnative.dev/), [React for terminal](https://github.com/vadimdemedes/ink))
+- сам React никак не связан ни с Web ни с DOM
+- для работы с DOM(HTML) используется отдельный пакет — [react-dom](https://reactjs.org/docs/react-dom.html)
+
+
+
+### Из чего состоит приложение на React?
+
+[React-элементы](https://ru.reactjs.org/docs/glossary.html) — это составляющие блоки React-приложений. Их **можно перепутать с более известной концепцией «компонентов»**, но в отличие от компонента, элемент описывает то, что вы хотите увидеть на экране.
+React-элементы иммутабельны.
+
+
+
+Представление React элементов через [`createElement`](https://ru.reactjs.org/docs/react-api.html#createelement)
+
+```jsx
+React.createElement(type, [props], [...children]);
+```
+
+
+
+```tsx [1-30]
+React.createElement(
+ "div",
+ {
+ className: "wrapper",
+ style: "margin: 5px",
+ },
+ React.createElement(
+ "button",
+ {
+ onClick: function onClick() {
+ return alert("Click");
+ },
+ className: "btn",
+ },
+ "Click me"
+ )
+);
+```
+
+
+
+```jsx [1-30]
+// jsx:
+// https://bit.ly/3gCKQOI
+
+
+
+```
+
+
+
+[React-компоненты](https://ru.reactjs.org/docs/glossary.html) — это маленькие, повторно используемые части кода, которые возвращают React-элементы для отображения на странице. Самый простой React-компонент — JavaScript функция, которая возвращает элементы React.
+
+
+
+```tsx [1-50]
+// компонент
+const Button = ({ name }) => {
+ return React.createElement(
+ "button",
+ {
+ onClick: function onClick() {
+ return alert("Click");
+ },
+ className: "btn",
+ },
+ name
+ );
+};
+
+// применение компонента
+React.createElement(
+ "div",
+ {
+ className: "wrapper",
+ style: "margin: 5px",
+ },
+ React.createElement(Button, {
+ name: "Click me",
+ })
+);
+```
+
+
+
+```jsx [1-30]
+const Button = ({ name }) => (
+
+);
+
+// применение компонента
+
+
+
;
+```
+
+
+
+Чтобы [отрисовать элемент на странице](https://ru.reactjs.org/docs/rendering-elements.html) нужен пакет [`react-dom`](https://ru.reactjs.org/docs/react-dom.html)
+
+```jsx [1-10]
+const element = Hello, world
;
+
+const root = ReactDOM.createRoot(document.getElementById("root"));
+root.render(element);
+```
+
+
+
+### [Пример работы с React элементами](https://stackblitz.com/edit/basic-react-dom-novgwc?file=index.js)
+
+
+
+Для поддержки синтаксиса `JSX` нужен пакет [@babel/preset-react](https://babeljs.io/docs/en/babel-preset-react). Но при этом вполне можно обходиться и [вообще без JSX](https://ru.reactjs.org/docs/react-without-jsx.html)
+
+
+
+### Вопросы?
+
+
+
+### JSX
+
+
+
+`JSX` — синтаксический сахар для функции `React.createElement`
+
+> JSX — расширение языка JavaScript. Мы рекомендуем использовать его, когда требуется объяснить React, как должен выглядеть UI. JSX напоминает язык шаблонов, наделённый силой JavaScript.
+
+
+
+[Знакомство с JSX](https://ru.reactjs.org/docs/introducing-jsx.html)
+
+[JSX в деталях](https://ru.reactjs.org/docs/jsx-in-depth.html)
+
+
+
+#### [Немного о правилах JSX](https://stackblitz.com/edit/react-basic-jsx-otus?file=jsxExamples.jsx)
+
+
+
+```tsx [1-30]
+// 1. Объявление компонентов
+
+// 1.1 Как функция
+const Cmp1 = (props) => {props.name}
;
+
+// 1.2 Как класс
+class Cmp2 extends React.Component {
+ render() {
+ return ;
+ }
+}
+```
+
+
+
+```tsx [1-30]
+// 2. Использование компонентов
+
+// 2.1 С закрывающим тегов
+
+
+// 2.2 С самозакрывающимся тегом (если нет дочерних элементов)
+
+```
+
+
+
+```tsx [1-30]
+// 3. Передача свойств в компонент
+
+// 3.1 Все свойства пишутся в {}
+
+
+
+// 3.2 Строковые литералы передаются как-есть
+
+
+// 3.3 Булевые true свойства можно передавать просто указанием
+
+```
+
+
+
+```tsx [1-30]
+// 4 Комментарий
+
+// 4.1 Вокруг компонентов/их методов - обычный js
+// так
+const Cmp3 = (props /* или так */) => {props.name}
;
+
+// 4.2 Внутри jsx
+const Cmp4 = (props) => (
+
+ {/* вот так */}
+ {props.name}
+
+);
+```
+
+
+
+```tsx [1-30]
+// 5 Выражения
+
+// jsx - subset js, выражения пишутся внутри {} (как в примере с свойствами компонентов)
+const Cmp5 = (props) => (
+
+ {props.disabled ? No way : }
+ {props.names.map((name) => (
+
+ ))}
+ {props.someFlag && }
+
+);
+```
+
+
+
+```tsx
+// Важно! (ну или было важно)
+import React from "react";
+```
+
+Начиная с React 17 появилась [возможность](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) использования новой JSX трансформации
+
+
+
+### Вопросы?
+
+
+
+### Тестирование
+
+
+
+- Подход от реализации - [Enzyme](https://enzymejs.github.io/enzyme/)
+
+- Подход от пользователя - [RTL](https://testing-library.com/docs/react-testing-library/intro/)
+
+
+
+### React-testing-library
+
+> The React Testing Library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils
+
+
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ // Render component
+ render();
+ // Debug info
+ screen.debug();
+ // For debugging using testing-playground,
+ // screen exposes this convenient method
+ // which logs a URL that can be opened in a browser
+ screen.logTestingPlaygroundURL();
+ });
+});
+```
+
+
+
+#### Вывод
+
+> RTL используется для взаимодействия с вашими компонентами React так, как это делает человек. То, что видит человек, - это просто визуализированный HTML из ваших компонентов React, поэтому вы видите эту структуру HTML как результат
+
+```html [1-30]
+
+
+
+```
+
+
+
+### Выбор элементов
+
+
+
+[jest-dom](https://github.com/testing-library/jest-dom/)
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ expect(screen.getByText("Search:")).toBeInTheDocument();
+ });
+});
+```
+
+
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ // неявная проверка
+ // getByText выбрасывает исключение если элемент не найден
+ screen.getByText("Search:");
+
+ // явная проверка - рекомендуется
+ expect(screen.getByText("Search:")).toBeInTheDocument();
+ });
+});
+```
+
+
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ // упадет
+ expect(screen.getByText("Search")).toBeInTheDocument();
+
+ // пройдет
+ expect(screen.getByText("Search:")).toBeInTheDocument();
+
+ // пройдет
+ expect(screen.getByText(/Search/)).toBeInTheDocument();
+ });
+});
+```
+
+
+
+### Возможности для поиска
+
+`getByRole` используется для поиска по [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label). Но, [у некоторых HTML элементов есть неявные роли](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ screen.getByRole("");
+ });
+});
+```
+
+
+
+```[1-30]
+Unable to find an accessible element with the role ""
+
+Here are the accessible roles:
+
+document:
+
+Name "":
+
+
+--------------------------------------------------
+textbox:
+
+Name "Search:":
+
+
+--------------------------------------------------
+```
+
+
+
+```js
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
+ });
+});
+```
+
+
+
+### Есть более специфичные способы поиска:
+
+`getByLabelText`:
+
+```html
+
+```
+
+`getByPlaceholderText`:
+
+```html
+
+```
+
+`getByAltText`:
+
+```html
+
+```
+
+`getByDisplayValue`:
+
+```html
+
+```
+
+
+
+И другие
+
+- getByText
+- getByRole
+- getByLabelText
+- getByPlaceholderText
+- getByAltText
+- getByDisplayValue
+
+
+
+### getBy vs queryBy
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ screen.debug();
+
+ // fails
+ expect(screen.getByText(/Searches for JavaScript/)).toBeNull();
+ });
+});
+```
+
+
+
+**getBy** выбрасывает исключение если элемент не найден.
+
+Для работы и проверки элементов, которых нет - можно использовать **queryBy**
+
+
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
+ });
+});
+```
+
+
+
+### findBy делает асинхронный поиск (И ожидание) элементов
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", async () => {
+ render();
+
+ expect(screen.queryByText(/Signed in as/)).toBeNull();
+
+ expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
+ });
+});
+```
+
+
+
+Для коллекций элементов есть
+
+- getAllBy
+- queryAllBy
+- findAllBy
+
+
+
+### Assertive Functions
+
+[jest-dom](https://github.com/testing-library/jest-dom/)
+
+- toBeDisabled
+- toBeEnabled
+- toBeEmpty
+- toBeEmptyDOMElement
+- toBeInTheDocument
+- toBeInvalid
+- toBeRequired
+- and etc...
+
+
+
+### [Testing-playground](https://testing-playground.com/)
+
+
+
+### Вопросы?
+
+
+
+### FIRE EVENT
+
+```js [1-30]
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", () => {
+ render();
+
+ screen.debug();
+
+ fireEvent.change(screen.getByRole("textbox"), {
+ target: { value: "JavaScript" },
+ });
+
+ screen.debug();
+ });
+});
+```
+
+
+
+```js [1-30]
+import React from "react";
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", async () => {
+ render();
+
+ // wait for the user to resolve
+ // needs only be used in our special case
+ await screen.findByText(/Signed in as/);
+
+ expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
+
+ fireEvent.change(screen.getByRole("textbox"), {
+ target: { value: "JavaScript" },
+ });
+
+ expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument();
+ });
+});
+```
+
+
+
+### React Testing Library: User Event
+
+> userEvent API имитирует реальное поведение браузера более точно, чем fireEvent API. Например, fireEvent.change() запускает только событие изменения, тогда как userEvent.type запускает событие изменения, а также события keyDown, keyPress и keyUp.
+
+
+
+```js [1-30]
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import App from "./App";
+
+describe("App", () => {
+ test("renders App component", async () => {
+ render();
+
+ // wait for the user to resolve
+ await screen.findByText(/Signed in as/);
+
+ expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
+
+ await userEvent.type(screen.getByRole("textbox"), "JavaScript");
+
+ expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument();
+ });
+});
+```
+
+
+
+### Обработчики событий
+
+
+
+
+
+```js
+import React from "react";
+
+function Search({ value, onChange, children }) {
+ return (
+
+
+
+
+ );
+}
+```
+
+
+
+```js [1-30]
+import React from "react";
+import Search from "./Search";
+// FireEvent
+describe("Search", () => {
+ test("calls the onChange callback handler", () => {
+ const onChange = jest.fn();
+
+ render(
+
+ Search:
+
+ );
+
+ fireEvent.change(screen.getByRole("textbox"), {
+ target: { value: "JavaScript" },
+ });
+
+ expect(onChange).toHaveBeenCalledTimes(1);
+ });
+});
+```
+
+
+
+```js [1-30]
+import React from "react";
+import Search from "./Search";
+// UserEvent
+describe("Search", () => {
+ test("calls the onChange callback handler", async () => {
+ const onChange = jest.fn();
+
+ render(
+
+ Search:
+
+ );
+
+ await userEvent.type(screen.getByRole("textbox"), "JavaScript");
+
+ expect(onChange).toHaveBeenCalledTimes(10);
+ });
+});
+```
+
+
+
+Вопросы?
+
+
+
+### CRA
+
+
+
+[CRA](https://create-react-app.dev/) - инструмент от [facebook](https://github.com/facebook/create-react-app) для старта разработки приложений
+
+
+
+```
+npx create-react-app my-app
+cd my-app
+npm start
+```
+
+c TS:
+
+```
+npx create-react-app my-app --template typescript
+```
+
+
+
+Скрывает от пользователя все настройка за [`react-scripts`](https://github.com/facebook/create-react-app/tree/master/packages/react-scripts)
+
+
+
+Для открытия настроек есть команда **eject**
+
+
+
+### На курсе мы НЕ будем использовать CRA!
+
+
+
+Вопросы?
+
+
+
+### [Storybook](https://storybook.js.org/)
+
+
+
+### [Loki](https://loki.js.org/)
+
+
+
+### Дополнительные материалы
+
+0. https://ru.reactjs.org/docs/getting-started.html
+1. https://pomb.us/build-your-own-react/
+2. https://jasonformat.com/wtf-is-jsx/
+3. https://github.com/pomber/didact
+4. [Paul O Shannessy - Building React From Scratch](https://www.youtube.com/watch?v=_MAD4Oly9yg)
+5. [YT: Автотесты. Модульное тестирование – Дмитрий Андриянов](https://www.youtube.com/watch?v=DFLXBdfnAeE)
+6. [Начало работы со Storybook](https://www.youtube.com/watch?v=lUf8qC_xFHo)