1. Що ми взагалі тестуємо в ChatGPT App (і що не тестуємо)
У класичному веб‑застосунку все досить прозоро: UI → backend → база даних. Зазвичай ми пишемо unit‑тести для функцій, інтеграційні — для API, а E2E — для сценарію на кшталт «користувач пройшов увесь шлях».
У ChatGPT App усе трохи складніше:
Пользователь ↔ ChatGPT UI ↔ Виджет (Apps SDK, React)
↘
MCP-сервер (tools/resources)
↘
ACP / backend / внешние API
Модель усередині ChatGPT вирішує, коли викликати ваш suggest_gifts, з якими аргументами, як відобразити structuredContent із MCP і коли показати ваш віджет.
Для тестування зручно поділити все на два шари:
- Infrastructure tests — те, чим ми займаємося в цій лекції. Ми перевіряємо, що:
- код віджета не ламається від дій користувача;
- MCP‑інструменти приймають і повертають дані у форматі, який обіцяний у схемах;
- ACP‑ендпоїнти й вебхуки «живі» та не падають на типовому JSON.
- AI behavior evals — те, що буде в модулі 20. Там ми вже дивимося, що саме відповідає модель: чи адекватно пояснює, чи правильно добирає подарунок за змістом, чи не галюцинує.
Сьогодні формула проста:
«Тестуємо все навколо LLM, але не саму LLM».
Саме тому в плані курсу для цієї теми окремо наголошено: «Ми не тестуємо GPT‑відповідь дослівно. Ми тестуємо інфраструктуру навколо неї і контракти даних».
Щоб не потонути в деталях, використаємо просту «піраміду» тестів для GiftGenius.
graph TD A["Unit-тести
utils, бізнес-логіка tools"] --> B[Contract-тести
Zod/JSON Schema, webhooks] B --> C[E2E / UI-тести
віджет + MCP без ChatGPT] C --> D["Smoke у CI
"воно взагалі живе?""] style A fill:#e0f7fa,stroke:#00838f,stroke-width:1px style B fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px style C fill:#fff3e0,stroke:#ef6c00,stroke-width:1px style D fill:#ffebee,stroke:#c62828,stroke-width:1px
Зараз пройдемося кожним рівнем і водночас доповнимо наш навчальний GiftGenius тестами. А наприкінці зберемо перелік типових помилок, які найчастіше трапляються під час тестування ChatGPT App.
2. Unit‑тести: ділимо GiftGenius на маленькі шматочки
Що вважати unitʼом у ChatGPT App
Unit‑тест у нашому стеку — це перевірка невеликої ізольованої частини логіки. Без реальної мережі, без бази даних і, за можливості, без виклику самого MCP‑фреймворку.
У GiftGenius це може бути:
- функція підрахунку «релевантності подарунка»;
- фільтр, який прибирає товари без ціни або з непридатною валютою;
- конвертер валют;
- перетворювач із «сирого» обʼєкта товару в GiftCardProps для UI.
Якщо говорити відверто, логіку самих MCP‑інструментів теж варто дробити. Обробник маршруту MCP — це тонка обгортка, яка викликає чисту функцію з бізнес‑логікою. А в unit‑тестах ми перевіряємо саме чисту функцію.
Приклад: функція ранжування подарунків
Уявімо, що в нас є утиліта scoreGift, яка на основі цінового діапазону й популярності виставляє «оцінку»:
// src/lib/scoreGift.ts
export type Gift = {
id: string;
price: number;
popularity: number; // 0..1
};
export function scoreGift(gift: Gift, maxPrice: number): number {
if (gift.price > maxPrice) return 0;
const priceScore = 1 - gift.price / maxPrice;
return Math.round((priceScore * 0.6 + gift.popularity * 0.4) * 100);
}
Пишемо unit‑тест на Jest (у Vitest він буде майже таким самим):
// src/lib/scoreGift.test.ts
import { scoreGift } from './scoreGift';
test('scoreGift занижує оцінку для дорогих подарунків', () => {
const cheap = { id: 'c', price: 50, popularity: 0.5 };
const expensive = { id: 'e', price: 100, popularity: 0.5 };
const max = 100;
const cheapScore = scoreGift(cheap, max);
const expensiveScore = scoreGift(expensive, max);
expect(cheapScore).toBeGreaterThan(expensiveScore);
});
Тут добре видно базовий підхід «Arrange – Act – Assert» (підготували дані, викликали функцію, перевірили результат). Це саме той структурний шаблон, який варто застосовувати й у складніших тестах.
Винесення бізнес‑логіки з MCP‑обробника
Зараз у вас, найімовірніше, є щось на кшталт:
// app/mcp/route.ts — сильно спрощено
import { createMcpServer } from '@modelcontextprotocol/sdk';
import { scoreGift } from '@/lib/scoreGift';
server.tool('suggest_gifts', {
// ...
handler: async ({ input }) => {
const gifts = await fetchFromCatalog(input);
const scored = gifts
.map(g => ({ ...g, score: scoreGift(g, input.maxPrice) }))
.sort((a, b) => b.score - a.score);
return { gifts: scored.slice(0, 10) };
},
});
Unit‑тест для scoreGift ми вже написали. Але хочеться протестувати й повнішу логіку: «функція, яка бере список подарунків і повертає відсортований топ‑10». Винесімо її в окремий модуль.
// src/lib/rankGifts.ts
import { scoreGift, Gift } from './scoreGift';
export function rankGifts(gifts: Gift[], maxPrice: number) {
return gifts
.map(g => ({ ...g, score: scoreGift(g, maxPrice) }))
.sort((a, b) => b.score - a.score)
.slice(0, 10);
}
І тест:
// src/lib/rankGifts.test.ts
import { rankGifts } from './rankGifts';
test('rankGifts повертає максимум 10 подарунків за спаданням score', () => {
const gifts = Array.from({ length: 20 }, (_, i) => ({
id: `g${i}`,
price: 10 + i,
popularity: 0.5,
}));
const result = rankGifts(gifts, 100);
expect(result).toHaveLength(10);
expect(result[0].score).toBeGreaterThanOrEqual(result[9].score);
});
Такі unit‑тести швидкі й недорогі та дають оперативний зворотний звʼязок. Саме тому їх і радять робити «широкою основою піраміди тестування» для MCP‑сервісів.
Unit‑тести для MCP‑інструментів: мокаємо зовнішні API
Поширена помилка — намагатися «unit‑тестувати» обробник MCP‑інструмента разом із реальними HTTP‑запитами до каталогу, Stripe тощо. У підсумку тест стає повільним і крихким.
Найкращий варіант — залишити в обробнику лише «склейку» (wiring), а всю складну логіку винести у функції, які ми вже тестуємо окремо. Якщо ж дуже хочеться протестувати сам handler, підміняйте залежності моками. Це якраз те, що рекомендують у докладних оглядах із тестування MCP: мокати зовнішні API в tool‑handlers.
3. Contract‑тести: Zod/JSON Schema як «договір» із моделлю та ACP
Що таке contract‑тест у нашому контексті
З unit‑логікою розібралися: маленькі чисті функції в нас під контролем. Наступний шар піраміди — переконатися, що сервіси й далі розуміють одне одного за JSON‑контрактами. Це і є contract‑тести.
Контрактне тестування — це перевірка того, що дві сторони, які обмінюються даними, і досі розуміють одна одну. Фокус тут не на внутрішніх алгоритмах, а на формі й сенсі JSON: поля, типи, обовʼязковість.
У ChatGPT App таких контрактів багато:
- ChatGPT ↔ MCP: inputSchema і outputSchema MCP‑інструментів.
- MCP ↔ commerce‑API (ACP): формат запитів create_checkout_session, структура відповідей.
- ACP ↔ наш бекенд через вебхуки: order.created, payment_failed тощо.
Якщо ви змінюєте схему, але забуваєте оновити код (або навпаки — змінюєте код, а схема лишається старою), виникає «тихий» розрив. Модель продовжує надсилати старий JSON, а ваш код уже очікує нове поле — і падає під час виконання. Саме такі ситуації contract‑тести мають ловити ще до продакшену.
Zod як єдине джерело правди
У JavaScript/TypeScript‑екосистемі для цього чудово підходить Zod, який ви вже використовували з MCP. SDK уміє перетворювати Zod‑схеми на JSON Schema для оголошення інструментів.
Наприклад, опишемо схему подарунка й результату рекомендації:
// src/schemas/gift.ts
import { z } from 'zod';
export const GiftSchema = z.object({
id: z.string(),
title: z.string(),
price: z.number().nonnegative(),
currency: z.string().length(3),
url: z.string().url(),
});
export const SuggestGiftsResultSchema = z.object({
gifts: z.array(GiftSchema).min(1),
});
Типи для коду отримуємо через z.infer:
export type Gift = z.infer<typeof GiftSchema>;
export type SuggestGiftsResult = z.infer<typeof SuggestGiftsResultSchema>;
Це вже своєрідна контрактна перевірка під час компіляції (compile‑time): якщо ви десь у коді спробуєте присвоїти currency: 123, TypeScript «засвітиться» й нагадає, що це має бути string.
Runtime‑contract‑тести для схем
Ще надійніше захищають тести під час виконання (runtime): вони проганяють реальні (або близькі до реальних) приклади даних через схеми.
// src/schemas/gift.test.ts
import { GiftSchema, SuggestGiftsResultSchema } from './gift';
test('GiftSchema приймає валідний товар', () => {
const sample = {
id: '123',
title: 'Горнятко з котом',
price: 19.99,
currency: 'USD',
url: 'https://example.com/gift/123',
};
expect(() => GiftSchema.parse(sample)).not.toThrow();
});
test('SuggestGiftsResultSchema відхиляє порожній список подарунків', () => {
const badResult = { gifts: [] };
expect(() => SuggestGiftsResultSchema.parse(badResult)).toThrow();
});
Чому це важливо:
- якщо ви в промптах або документації показуєте приклади JSON для моделі, їх можна покласти прямо в такі тести й гарантувати, що «приклад не бреше»;
- якщо ви змінюєте схему (наприклад, робите поле url обовʼязковим), тести одразу підсвітять усі старі приклади та фікстури, які вже невалідні.
Офіційні рекомендації щодо Apps SDK прямо підкреслюють: structured content має відповідати оголошеній outputSchema, інакше модель може його не зрозуміти. Тести на схеми — перша лінія оборони від таких розбіжностей.
Контракти webhooks і ACP
Цей самий принцип працює і для вебхуків та ACP‑ендпоїнтів. Нехай у нас є OrderCreated:
// src/schemas/acp.ts
import { z } from 'zod';
export const OrderCreatedSchema = z.object({
id: z.string(),
userId: z.string(),
totalAmount: z.number(),
currency: z.string().length(3),
status: z.literal('created'),
});
Тест:
// src/schemas/acp.test.ts
import { OrderCreatedSchema } from './acp';
test('OrderCreatedSchema валідовує зразок вебхука', () => {
const sample = {
id: 'ord_1',
userId: 'user_42',
totalAmount: 59.99,
currency: 'USD',
status: 'created',
};
expect(() => OrderCreatedSchema.parse(sample)).not.toThrow();
});
Далі в обробнику вебхука ви насамперед робите OrderCreatedSchema.parse(body) — і вже впевнені, що працюєте з валідним обʼєктом.
OpenAI у своєму регресійному переліку перевірок для Appʼів також радить підтримувати схеми актуальними в міру розвитку застосунку. Contract‑тести якраз гарантують, що ви про це не забудете.
4. Тестування віджета і «майже E2E»: як обійтися без chatgpt.com
Unit‑тести тримають у порядку логіку, contract‑тести — форму даних між сервісами. Але на цьому піраміда не закінчується: нам ще потрібно перевірити, що шлях користувача через віджет і MCP справді працює як єдине ціле. Для ChatGPT App це буде особливий, «майже E2E» формат.
Чому не можна просто «запустити Playwright» на ChatGPT
Перша думка зазвичай така: «Давайте відкриємо https://chatgpt.com, запустимо віджет, пройдемо весь сценарій „підібрати подарунок → оформити замовлення“ Playwrightʼом — і матимемо справжній E2E».
На жаль, ні.
Проблеми:
- автоматизований прогін по chatgpt.com порушує ToS;
- є захист (Cloudflare, 2FA тощо), який дуже не любить ботів із CI;
- поведінка моделі варіативна: сьогодні вона викликала ваш suggest_gifts, а завтра вирішила обмежитися текстовою відповіддю.
Тому для ChatGPT App E2E‑тест трактують ширше. Ми тестуємо повний шлях усередині свого застосунку: віджет + MCP + ACP — але без реального ChatGPT UI і без реальної моделі.
У докладних посібниках зазвичай пропонують таку стратегію: окремо тестувати MCP‑сервер headless‑клієнтом, а віджет — у «тестовому хості» з підміненою window.openai.
Тестування віджета як React‑компонента
Базовий варіант — React Testing Library. Нам потрібно:
- Відкрити компонент GiftGeniusWidget.
- Підставити йому фейковий window.openai з потрібними методами (callTool, openExternal тощо).
- Взаємодіяти з ним як користувач: натискати кнопки, вводити текст.
- Перевірити, що callTool викликано з правильними аргументами і що UI показує очікуваний результат.
Припустімо, у нас є спрощений віджет:
// src/app/GiftGeniusWidget.tsx
'use client';
import React from 'react';
export function GiftGeniusWidget() {
const [loading, setLoading] = React.useState(false);
async function handleClick() {
setLoading(true);
await (window as any).openai.callTool('suggest_gifts', {
occasion: 'birthday',
});
setLoading(false);
}
return (
<div>
<button onClick={handleClick}>Підібрати подарунок</button>
{loading && <p>Секунду, підбираю ідеї...</p>}
</div>
);
}
Тест:
// src/app/GiftGeniusWidget.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { GiftGeniusWidget } from './GiftGeniusWidget';
test('кнопка викликає suggest_gifts через window.openai.callTool', async () => {
const callToolMock = vi.fn().mockResolvedValue({});
(window as any).openai = { callTool: callToolMock };
render(<GiftGeniusWidget />);
const button = screen.getByText('Підібрати подарунок');
await fireEvent.click(button);
expect(callToolMock).toHaveBeenCalledWith('suggest_gifts', {
occasion: 'birthday',
});
});
Тут ми повністю контролюємо оточення:
- жодного справжнього ChatGPT;
- жодної мережі;
- чистий, швидкий тест, який перевіряє звʼязок «UI → window.openai».
У документації щодо Apps SDK саме це і рекомендують: мокати window.openai під час тестування віджета, щоб не залежати від реального середовища.
E2E‑light із Playwright: Next.js + MCP
Наступний рівень — ми запускаємо локально Next.js‑застосунок (як у Dev Mode), але відкриваємо його не через ChatGPT, а напряму з браузера тесту.
Сценарій, який має сенс перевірити:
- Відкриваємо сторінку /widget (або / — залежно від того, як у вас влаштований проєкт).
- Імітуємо мінімум кроків: обираємо тип подарунка, тиснемо кнопку «Показати ідеї».
- Перевіряємо, що віджет показав картки подарунків.
- (Опційно) натискаємо на картку, тиснемо «Перейти до оплати» і переконуємося, що ACP‑мок повернув успіх.
Міні‑приклад Playwright‑тесту:
// tests/e2e/gift-flow.spec.ts
import { test, expect } from '@playwright/test';
test('користувач може вибрати подарунок і побачити результати', async ({ page }) => {
await page.goto('http://localhost:3000/widget');
await page.click('text=Подарунок на день народження');
await page.click('text=Підібрати');
await page.waitForSelector('[data-testid="gift-card"]');
const cards = await page.locator('[data-testid="gift-card"]').all();
expect(cards.length).toBeGreaterThan(0);
});
У реальному проєкті до цього додадуться:
- запуск npm run dev або окремого test‑server у beforeAll Playwrightʼа;
- моки для MCP/ACP, щоб не чіпати продакшн‑сервіси.
Але навіть такий простий сценарій уже ловить типові «злами» між віджетом і MCP: неправильний URL, CORS‑помилки, некоректні structuredContent тощо.
5. Smoke‑тести в CI: перевіряємо, що «воно взагалі запускається»
Залишився верхній, найпростіший шар нашої піраміди — smoke‑тести. Вони не перевіряють увесь сценарій, як E2E‑light, а просто дають відповідь: застосунок узагалі «живий» і підіймається перед деплоєм?
Smoke vs повний E2E
Про «ручний» smoke‑тест ви чули ще в другому модулі: ми тоді запускали найперший «Hello GiftGenius», перевіряли, що віджет рендериться, ChatGPT його бачить, а кнопка відкриває посилання. Мета була проста: переконатися, що Dev Mode + тунель + конфігурація Apps SDK налаштовані коректно.
Тепер завдання схоже, але автоматизоване й у CI:
- ми не намагаємося змоделювати всі сценарії користувача;
- ми не взаємодіємо зі справжнім ChatGPT;
- ми лише перевіряємо, що:
- Next.js‑застосунок стартує;
- MCP‑сервер відповідає хоча б на базовий tools/list / tools/call;
- ACP‑ендпоїнт «живий» і віддає 200 на тестовий JSON.
Це особливо важливо перед деплоєм у продакшн або перед надсиланням нової версії в Store: простіше зловити «все впало й не стартує» в CI, ніж дізнатися про це від користувачів.
Приклад smoke‑тесту для MCP‑tools
Припустімо, у нас є допоміжний модуль, що підіймає MCP‑сервер у тесті або використовує MCP‑клієнт із SDK. Загалом тест виглядає так:
// tests/smoke/mcp-tools.smoke.test.ts
import { createTestMcpClient } from './testClient';
test('MCP відповідає на tools.list і tools.call(suggest_gifts)', async () => {
const client = await createTestMcpClient(); // підіймає сервер або конектиться до нього
const tools = await client.listTools();
expect(tools.some(t => t.name === 'suggest_gifts')).toBe(true);
const result = await client.callTool('suggest_gifts', {
occasion: 'birthday',
budget: { currency: 'USD', max: 50 },
});
expect(result.gifts.length).toBeGreaterThan(0);
});
У докладніших оглядах про тестування MCP часто радять саме такий підхід: використовувати MCP‑клієнт у тестах, щоб перевірити повний цикл JSON‑RPC — list → call → відповідь.
Реалізацію createTestMcpClient можна сховати в утиліти: вона або стартує сервер у тому самому процесі, або підʼєднується до вже запущеного екземпляра.
Smoke‑тест для ACP/checkout
Аналогічно можна написати найпростіший тест для commerce‑шару, не імітуючи реальну оплату:
// tests/smoke/acp.smoke.test.ts
import fetch from 'node-fetch';
test('ACP test-intent повертає 200', async () => {
const res = await fetch('http://localhost:3000/api/acp/test-intent', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
amount: 10,
currency: 'USD',
}),
});
expect(res.ok).toBe(true);
});
Тут не так важливо, що саме робить test-intent — він може просто перевіряти доступ до бази даних і повертати {"status":"ok"}. Головне, що CI зловить:
- забутий env‑ключ;
- зламаний роут;
- некоректний парсинг JSON.
Мінімальний pipeline CI
Детально про CI/CD буде в модулях про деплой, але базовий pipeline може виглядати так (на прикладі GitHub Actions):
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test # unit + contract
- run: npm run test:e2e # e2e/ui
- run: npm run test:smoke # smoke mcp/acp
Команди npm run test:e2e і npm run test:smoke усередині вже можуть запускати dev‑сервер, чекати на його готовність і стартувати Playwright / Node‑скрипти.
6. Міні‑карта тестів для GiftGenius
Щоб не загубитися, зберемо все в одну таблицю: що ми тестуємо на кожному рівні й які запитання це закриває.
| Рівень | Приклади для GiftGenius | Інструменти | На яке питання відповідає |
|---|---|---|---|
| Unit | scoreGift, rankGifts, валідатори бюджету | Jest / Vitest | Логіка працює правильно? |
| Contract (schemas) | Zod‑схеми Gift, SuggestGiftsResult, OrderCreated | Zod, AJV | Ми все ще «говоримо» однією JSON‑мовою з GPT/ACP? |
| UI/Component | Поведінка віджета під час натискання, виклик window.openai.callTool | React Testing Library | UI викликає правильні дії? |
| E2E‑light | Користувач пройшов сценарій вибору подарунка й побачив картки | Playwright/Cypress | Усі частини GiftGenius складаються в робочий сценарій? |
| Smoke у CI | MCP відповідає на tools.list/call, ACP test-intent 200 | Node‑скрипти, MCP client | Застосунок узагалі «живий» і правильно звʼязаний? |
Ця звʼязка — той самий «мінімально життєздатний набір тестів» для ChatGPT App, про який ідеться в плані модуля: без повноцінного enterprise‑QA‑штату, але з базовою гарантією, що продакшн не ламається від кожної дрібниці.
7. Типові помилки під час тестування ChatGPT App
Помилка № 1: намагатися детерміновано тестувати відповіді моделі.
Іноді розробники намагаються написати тести на кшталт: «очікую, що GPT відповість рядком Ось 5 ідей подарунків». Такі тести крихкі за визначенням: модель не зобовʼязана повторювати формулювання слово в слово, та й сама модель може оновитися. У цьому модулі ми взагалі не чіпаємо зміст відповідей — лише перевіряємо, що інструменти викликаються, схеми валідні, а сценарій не падає. Оцінювання якості текстів — окрема дисципліна (М20, LLM‑evals).
Помилка № 2: відсутність contract‑тестів для MCP‑схем.
Дуже спокусливо описати Zod‑схему один раз і «забути» про неї. Потім ви додаєте в результат інструмента поле discount, оновлюєте код, але не оновлюєте схему. Модель продовжує надсилати старий формат, а ваш код уже очікує нове поле — і в продакшні починаються дивні падіння. Контрактні тести на Zod/JSON Schema якраз запобігають таким «тихим» збоям, тож нехтувати ними — популярний і дуже болючий промах.
Помилка № 3: намагатися ганяти E2E по chatgpt.com із CI.
Дехто все одно пробує: запускає Playwright проти реального ChatGPT, входить в обліковий запис, клацає по UI — і отримує бан від Cloudflare, нестабільні тести та потенційне порушення умов використання. Правильний шлях — тестувати власний Next.js‑хост + MCP в ізоляції, мокаючи window.openai і зовнішні API, як рекомендують гайди щодо Apps SDK і MCP.
Помилка № 4: писати лише E2E й забувати про unit‑рівень.
Іноді трапляється проєкт, де є один «великий» E2E‑тест, що проходить через пів застосунку, і нуль unit‑тестів. Такий підхід дає хибне відчуття захищеності: тест або зелений, або червоний, але локалізувати причину майже неможливо, і кожен запуск займає хвилини. Набагато ефективніше мати десятки швидких unit‑тестів для чистих функцій і кілька акуратних E2E‑light сценаріїв для критичних шляхів.
Помилка № 5: використовувати реальні зовнішні API у звичайних тестах.
Stripe, зовнішні каталоги, CRM — усе це чудово підходить для інтеграційних тестів у контрольованому середовищі, але не для звичайного npm test. Якщо ваші тести залежать від мережі, чужих rate‑limitʼів і чийогось продакшн‑сервера, вони падатимуть з причин, не повʼязаних із вашим кодом. Найкращий підхід — мокати зовнішнє API (через nock, msw тощо) і окремо мати кілька «живих» перевірок у спеціальному оточенні.
Помилка № 6: забувати про smoke‑тести перед розгортанням.
Зібрали функціональність, оновили MCP‑schema, підправили UI, натиснули «Deploy» — а Next.js не стартує, бо хтось зламав next.config або видалив .env. Без автоматизованих smoke‑тестів CI пропускає такі очевидні провали в продакшн. Один простий smoke‑suite, який перевіряє «сервер піднявся», «MCP відповідає на базовий виклик» і «ACP тестовий endpoint дає 200», економить години продакшн‑дебагу й чимало нервів.
Помилка № 7: надмірно ускладнювати тестовий контур на ранньому етапі.
Іноді, надихнувшись найкращими практиками великих компаній, хочеться одразу завести десяток середовищ, складні контрактні тести з генерацією даних, навантажувальні сценарії тощо. У підсумку команда витрачає тижні на інфраструктуру й перестає випускати функціональність. Для старту з ChatGPT App достатньо того «sanity suite», про який ми говорили: unit + contract + кілька E2E‑light + smoke у CI. Далі вже можна еволюціонувати в міру зростання трафіку й вимог.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ