Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AGENTS.md

Это учебный HTTP-сервер, на который **живьём ходят студенты** из курсов Хекслета:
HTTP API, протокол HTTP, Postman, js-playwright. Уроки и самостоятельные работы
HTTP API, протокол HTTP, Postman, js-playwright, курсы Selenium и Playwright. Уроки и самостоятельные работы
цитируют его ответы дословно, вплоть до значений полей.

Отсюда главное следствие, из которого растёт почти всё остальное в этом файле:
Expand Down Expand Up @@ -269,6 +269,16 @@ make deploy APP=http_example HOST=timeweb SKIP=caddy,cron

**Значения полей только на латинице.** Сервер один на все локали курсов, и русский текст в ответе читался бы как ошибка у испанского студента. По-русски здесь пишутся комментарии в коде и спецификациях, не данные.

## Учебные приложения для UI-тестов это тоже текст уроков

Префиксы `/java-selenium`, `/python-selenium` и `/python-playwright` обслуживают курсы Selenium и Playwright, ru и es. У каждого курса своё приложение (библиотека, трекер задач, табло вылетов), описанное в `apps` в `custom-server/src/ui-pages.ts`. Общее у них только устройство входа, сессии и флеш-сообщений. Уроки находят элементы по `id`, классам и `data-test` и сравнивают надписи дословно, поэтому переименование `#username`, `#book-moby-dick`, `.figcaption h5` или текста флеш-сообщения ломает примеры курса. Страницы и локаторы перечислены в README, их держит `test/ui-pages.test.ts`.

- **Тема приложения выбрана под уроки курса.** Новая страница появляется из сценария приложения: в библиотеке перетаскивают книгу на полку, на табло время зависит от часового пояса. Страница-тренажёр без сценария сюда не идёт.
- **Слова Hexlet в приложениях нет.** Это вымышленные сервисы, и студент тестирует их как чужой продукт.
- **Перетаскивание собрано на `mousedown` и `mouseup`.** `Actions.dragAndDrop` в Selenium эмулирует мышь, а события HTML5 `dragstart` и `drop` браузер на неё не шлёт, поэтому `draggable` пример молча не срабатывал бы.
- **Сессия это подписанная кука с путём курса, и курс записан в её значении.** Урок про куки сохраняет их после входа, открывает страницу в новом браузере, подкладывает куки руками и идёт в закрытый раздел. Кука одного курса не открывает раздел другого, неподписанное значение не открывает ничего.
- **Учётки напечатаны на страницах входа и в уроках.** Их смена это правка курсов.

## Набор данных не меняется

`POST` и `DELETE` отвечают 201 и 204, но список задач остаётся прежним: создание
Expand Down
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# http-example

Учебный HTTP-сервер Хекслета: [https://http.hexlet.app](https://http.hexlet.app). На него **живьём ходят студенты** из курсов HTTP API, протокол HTTP, Postman и js-playwright, а уроки и самостоятельные работы цитируют его ответы дословно, вплоть до значений полей.
Учебный HTTP-сервер Хекслета: [https://http.hexlet.app](https://http.hexlet.app). На него **живьём ходят студенты** из курсов HTTP API, протокол HTTP, Postman, js-playwright и курсов Selenium и Playwright, а уроки и самостоятельные работы цитируют его ответы дословно, вплоть до значений полей.

Отсюда главное свойство проекта: **ответы сервера это контент курса**, а не деталь реализации. Правка данных здесь делает неверным текст урока в другом репозитории, и по коду это не видно. Поэтому правила работы с проектом собраны в [AGENTS.md](./AGENTS.md), и читать их стоит до первой правки — там же разобрано, почему наборы данных такого размера, почему они не меняются и какие коды ответов несут уроки.

Expand All @@ -19,6 +19,39 @@

Обработчик `body` принимает JSON, `application/x-www-form-urlencoded` и multipart. Для примера с файлом он возвращает его содержимое как строку UTF-8. Лимиты multipart допускают один файл до 64 КиБ и до десяти текстовых полей по 4 КиБ. Исходное тестовое изображение взято из [httpbin](https://httpbin.org/image/png), который распространяется под [лицензией MIT](https://github.com/postmanlabs/httpbin/blob/master/LICENSE).

## Учебные приложения для UI-тестов

Курсы Selenium и Playwright тестируют маленькие приложения под префиксом своего курса. У каждого своя предметная область, и страницы в нём появились из её сценариев. Уроки ищут элементы и проверяют тексты дословно, поэтому локаторы и надписи этих страниц тоже контент курса.

**`/java-selenium`, библиотека City Library.** Курс разбирает действия пользователя, окна и фреймы, и у каждого действия в библиотеке есть смысл.

| Путь | Сценарий |
| --- | --- |
| `/java-selenium` | каталог, `h1` «City Library» |
| `/java-selenium/login` → `/my-shelf` | вход читателя `reader` / `books123`, полка с выданными книгами |
| `/java-selenium/covers` | обложки `.figure`, название `.figcaption h5` видно при наведении, двойной клик добавляет книгу в избранное |
| `/java-selenium/shelves` | книга `#book-moby-dick` перетаскивается на полку `#shelf-reading` |
| `/java-selenium/search` | поиск `#search` фильтрует каталог, Enter печатает число найденного в `#result` |
| `/java-selenium/return` | кнопка `#return-book` спрашивает `confirm` перед возвратом |
| `/java-selenium/preview` | «Open preview» открывает отрывок в новом окне |
| `/java-selenium/reader` | глава книги во фрейме `chapter`, абзац `#content` |

**`/python-selenium`, трекер Task Board.** Курс разбирает сессию, куки, ожидания и мобильное меню.

| Путь | Сценарий |
| --- | --- |
| `/python-selenium` | промо-страница, первая ссылка «Sign in» после названия |
| `/python-selenium/login` → `/tasks` | вход `student` / `tasks123`, список задач, меню `#menu-button` с пунктами `.menu-item` |

**`/python-playwright`, табло Departures.** Курс настраивает контекст браузера, и табло на эти настройки отвечает.

| Путь | Сценарий |
| --- | --- |
| `/python-playwright` | табло `#departures`, время вылета печатается в часовом поясе браузера, на узком экране строки складываются в карточки |
| `/python-playwright/login` → `/trips` | вход пассажира `passenger` / `flight123`, «My trips» |

У всех трёх одинаковое устройство входа. Форма `#login` с полями `#username` и `#password` и кнопкой `button.primary[data-test=login-button]`, флеш-сообщение `#flash`, выход по `/<курс>/logout`. Приложения описаны конфигом `apps` в `custom-server/src/ui-pages.ts`, страницы лежат фрагментами в `custom-server/assets/ui-pages/<курс>/`, префикс в них записан как `{{prefix}}`.

## Prerequisites

- [mise](https://mise.jdx.dev) (ставит Node, pnpm, Caddy и jactionlint по `mise.toml`)
Expand Down
15 changes: 15 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/covers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<main class="example">
<h3>Book covers</h3>
<p>Hover over a cover to see the book. Double-click the title to add it to favorites.</p>
<div class="figure"><div class="cover">MD</div><div class="figcaption"><h5>Moby Dick</h5><p>Herman Melville</p></div></div>
<div class="figure"><div class="cover">TI</div><div class="figcaption"><h5>Treasure Island</h5><p>Robert Louis Stevenson</p></div></div>
<div class="figure"><div class="cover">PP</div><div class="figcaption"><h5>Pride and Prejudice</h5><p>Jane Austen</p></div></div>
<p id="result"></p>
</main>
<script>
for (const title of document.querySelectorAll('.figcaption h5')) {
title.addEventListener('dblclick', () => {
document.getElementById('result').textContent = `Added to favorites: ${title.textContent}`;
});
}
</script>
13 changes: 13 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<main>
<h1 class="heading">City Library</h1>
<p>Find a book, put it on your shelf and read the first chapter online.</p>
<ul>
<li><a href="{{prefix}}/login">Reader login</a></li>
<li><a href="{{prefix}}/covers">Book covers</a></li>
<li><a href="{{prefix}}/shelves">Shelves</a></li>
<li><a href="{{prefix}}/search">Catalog search</a></li>
<li><a href="{{prefix}}/return">Return a book</a></li>
<li><a href="{{prefix}}/preview">Book preview</a></li>
<li><a href="{{prefix}}/reader">Chapter reader</a></li>
</ul>
</main>
5 changes: 5 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<main class="example">
<h3>Moby Dick</h3>
<p>Herman Melville, 1851.</p>
<a href="{{prefix}}/preview/moby-dick" target="_blank">Open preview</a>
</main>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<main class="example">
<h3>Moby Dick: preview</h3>
<p>Call me Ishmael. Some years ago, never mind how long precisely, having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.</p>
</main>
5 changes: 5 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/reader.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<main class="example">
<h3>Chapter reader</h3>
<p>Moby Dick, chapter 1. Loomings.</p>
<iframe id="chapter" name="chapter" title="Chapter 1" src="{{prefix}}/reader/chapter"></iframe>
</main>
11 changes: 11 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/reader_chapter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Chapter 1. Loomings</title>
</head>
<body>
<h4>Chapter 1. Loomings</h4>
<p id="content">Call me Ishmael.</p>
</body>
</html>
12 changes: 12 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/return.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<main class="example">
<h3>Return a book</h3>
<p>Moby Dick, Herman Melville. Due in 7 days.</p>
<button type="button" id="return-book">Return the book</button>
<p id="result"></p>
</main>
<script>
document.getElementById('return-book').addEventListener('click', () => {
const returned = confirm('Return Moby Dick to the library?');
document.getElementById('result').textContent = returned ? 'The book is returned.' : 'The book stays on your shelf.';
});
</script>
30 changes: 30 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<main class="example">
<h3>Catalog search</h3>
<label for="search">Title or author</label>
<input type="search" id="search" autocomplete="off" />
<ul id="books">
<li>Moby Dick, Herman Melville</li>
<li>Treasure Island, Robert Louis Stevenson</li>
<li>Pride and Prejudice, Jane Austen</li>
<li>Frankenstein, Mary Shelley</li>
</ul>
<p id="result"></p>
</main>
<script>
const input = document.getElementById('search');
const books = [...document.querySelectorAll('#books li')];
const filter = () => {
const query = input.value.trim().toLowerCase();
const found = books.filter((book) => {
book.hidden = !book.textContent.toLowerCase().includes(query);
return !book.hidden;
});
return found.length;
};
input.addEventListener('input', filter);
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
document.getElementById('result').textContent = `Found: ${filter()}`;
}
});
</script>
36 changes: 36 additions & 0 deletions custom-server/assets/ui-pages/java-selenium/shelves.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<main class="example">
<h3>Shelves</h3>
<p>Drag a book to the shelf you are reading from.</p>
<div id="shelves">
<section class="shelf" id="shelf-want">
<header>Want to read</header>
<div class="book" id="book-moby-dick">Moby Dick</div>
<div class="book" id="book-treasure-island">Treasure Island</div>
</section>
<section class="shelf" id="shelf-reading">
<header>Reading</header>
</section>
</div>
</main>
<script>
// Перетаскивание собрано на событиях мыши, а не на HTML5 drag and drop:
// Actions в Selenium эмулирует мышь, и событий dragstart браузер на неё не шлёт.
let dragged = null;
for (const book of document.querySelectorAll('.book')) {
book.addEventListener('mousedown', (event) => {
event.preventDefault();
dragged = book;
});
}
for (const shelf of document.querySelectorAll('.shelf')) {
shelf.addEventListener('mouseup', () => {
if (dragged && dragged.parentElement !== shelf) {
shelf.append(dragged);
}
dragged = null;
});
}
document.addEventListener('mouseup', () => {
dragged = null;
});
</script>
23 changes: 23 additions & 0 deletions custom-server/assets/ui-pages/python-playwright/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<main>
<a href="{{prefix}}/login">My trips</a>
<h1 class="heading">Departures</h1>
<p id="timezone">Times are shown in your time zone.</p>
<table id="departures">
<thead>
<tr><th>Flight</th><th>Destination</th><th>Time</th><th>Status</th></tr>
</thead>
<tbody>
<tr data-departure="2026-01-15T08:30:00Z"><td>SK 204</td><td>Lisbon</td><td class="time"></td><td>On time</td></tr>
<tr data-departure="2026-01-15T09:10:00Z"><td>SK 318</td><td>Oslo</td><td class="time"></td><td>Boarding</td></tr>
<tr data-departure="2026-01-15T11:45:00Z"><td>SK 527</td><td>Istanbul</td><td class="time"></td><td>Delayed</td></tr>
</tbody>
</table>
</main>
<script>
// Время вылета хранится в UTC и печатается в часовом поясе браузера, поэтому
// timezone_id контекста Playwright меняет то, что видно на табло.
const format = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit', timeZoneName: 'short' });
for (const row of document.querySelectorAll('#departures tbody tr')) {
row.querySelector('.time').textContent = format.format(new Date(row.dataset.departure));
}
</script>
5 changes: 5 additions & 0 deletions custom-server/assets/ui-pages/python-selenium/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<main>
<a href="{{prefix}}/login">Sign in</a>
<h1 class="heading">Task Board</h1>
<p>Plan the tests you are going to write and tick them off one by one.</p>
</main>
30 changes: 30 additions & 0 deletions custom-server/assets/ui-pages/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
body { font: 18px/1.5 system-ui, sans-serif; margin: 40px auto; padding: 0 24px; max-width: 760px; color: #18212d; }
a { color: #315acc; }
.brand { font-weight: 600; text-decoration: none; }
label { display: block; margin-top: 16px; }
input { display: block; box-sizing: border-box; width: 100%; margin-top: 8px; padding: 12px; font: inherit; border: 1px solid #9ca6b4; border-radius: 6px; }
button, .button { display: inline-block; margin-top: 16px; padding: 10px 24px; font: inherit; color: white; background: #315acc; border: 0; border-radius: 6px; cursor: pointer; text-decoration: none; }
.flash { margin: 16px 0; padding: 12px 16px; border-radius: 6px; }
.flash.success { background: #dff3e4; }
.flash.error { background: #fbe1e1; }
#menu { display: flex; gap: 16px; margin-top: 8px; }
#menu[hidden] { display: none; }
.figure { display: inline-block; margin: 16px 16px 0 0; vertical-align: top; width: 140px; }
.cover { height: 180px; display: grid; place-items: center; font-size: 36px; color: white; background: #6b4f3a; border-radius: 4px 10px 10px 4px; }
.figcaption { display: none; }
.figure:hover .figcaption { display: block; }
.figcaption h5 { margin: 8px 0 0; font-size: 16px; }
.figcaption p { margin: 0; font-size: 14px; color: #5c6675; }
#shelves { display: flex; gap: 24px; }
.shelf { flex: 1; min-height: 180px; padding: 12px; background: #f1ece4; border: 2px solid #b9a88f; border-radius: 8px; }
.shelf header { font-weight: 600; margin-bottom: 8px; }
.book { margin-top: 8px; padding: 8px 12px; background: white; border: 1px solid #b9a88f; border-radius: 4px; cursor: move; user-select: none; }
iframe { width: 100%; height: 200px; border: 1px solid #9ca6b4; border-radius: 6px; }
#departures { width: 100%; border-collapse: collapse; }
#departures th, #departures td { padding: 8px; text-align: left; border-bottom: 1px solid #d5dbe3; }
/* На узком экране строки табло складываются в карточки. */
@media (max-width: 600px) {
#departures thead { display: none; }
#departures tr { display: block; margin-bottom: 12px; padding: 8px; border: 1px solid #d5dbe3; border-radius: 8px; }
#departures td { display: block; border: 0; padding: 2px 0; }
}
2 changes: 2 additions & 0 deletions custom-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import fastifyCookie from '@fastify/cookie';
import rawConfig from '../../app.config.json' with { type: 'json' };
import setUpRpc from './rpc.ts';
import setUpBodyExamples from './body-examples.ts';
import setUpUiPages from './ui-pages.ts';
import setUpSpec from './openapi-spec.ts';

// title в app.config.json нет, и документация всегда отдавалась без заголовка.
Expand Down Expand Up @@ -113,6 +114,7 @@ export default async (app: FastifyInstance, _options: unknown) => {
setUpStaticAssets(app);

await app.register(setUpBodyExamples);
await app.register(setUpUiPages);

await setupDocs(app);

Expand Down
Loading
Loading