Appearance
Partizap — User Flows для E2E-тестирования
Этот документ описывает пользовательские сценарии для ручного прокликивания и последующей генерации автотестов через Gasoline MCP / Playwright.
Последняя сверка с backend API: 2026-03-28 Последняя сверка product-form flows (UF-08/09/10): 2026-04-09 (после merge DEV-286..DEV-319)
Статус готовности
| Flow | Приоритет | Backend | Frontend | Готов к тесту | E2E автотест |
|---|---|---|---|---|---|
| UF-01 Регистрация | Critical | ✅ | ✅ | ✅ | ✅ DEV-204 |
| UF-02 Логин | Critical | ✅ | ✅ | ✅ | ✅ DEV-204 |
| UF-03 Верификация email | High | ✅ | ✅ | ✅ | — |
| UF-04 Сброс пароля | High | ✅ | ✅ | ✅ | ✅ DEV-204 |
| UF-05 Каталог | Critical | ✅ | ✅ | ✅ | ✅ DEV-205 |
| UF-06 Поиск по авто | High | ✅ | ✅ | ✅ | ✅ DEV-205 |
| UF-07 Карточка товара | Critical | ✅ | ✅ | ✅ | ✅ DEV-205 |
| UF-08 Создание объявления | Critical | ✅ | ✅ | ✅ | ✅ DEV-206 |
| UF-09 Редактирование/публикация | Critical | ✅ | ✅ | ✅ | ✅ DEV-206 |
| UF-10 Управление фото | Medium | ✅ | ✅ | ✅ | ✅ DEV-206 |
| UF-11 Начало чата | High | ✅ | ✅ | ✅ | ✅ DEV-207 |
| UF-12 Обмен сообщениями | High | ✅ | ✅ | ✅ | ✅ DEV-207 |
| UF-13 Поиск по сообщениям | Medium | ✅ | ✅ | ✅ | ✅ DEV-210 |
| UF-14 Избранное | Medium | ✅ | ✅ | ✅ | ✅ DEV-208 |
| UF-15 Профиль продавца | Medium | ✅ | ✅ | ✅ | ✅ DEV-208 |
| UF-16 Сессии | Low | ✅ | ✅ | ✅ | ✅ DEV-213 |
| UF-17 Модерация | Critical | ✅ | ✅ | ✅ | ✅ DEV-207 |
| UF-18 Справочники | High | ✅ | ✅ | ✅ | ✅ DEV-221 |
| UF-19 Пользователи | Medium | ✅ | ✅ | ✅ | ✅ DEV-215 |
| UF-20 Дашборд | Medium | ✅ | ✅ | ✅ | ✅ DEV-215 |
| UF-21 Текстовый поиск и автокомплит | High | ✅ | ✅ | ✅ | ✅ DEV-205 |
| UF-22 Выбор города | Medium | ✅ | ✅ | ✅ | ✅ DEV-208 |
| UF-23 Смена пароля | Medium | ✅ | ✅ | ✅ | ✅ DEV-213 |
| UF-24 Бизнес-профиль | Low | ✅ | ✅ | ✅ | ✅ DEV-208 |
| UF-25 Публичная страница продавца | Medium | ✅ | ✅ | ✅ | ✅ DEV-212 |
Навигация
- UF-01: Регистрация нового пользователя
- UF-02: Авторизация существующего пользователя
- UF-03: Верификация email
- UF-04: Сброс пароля
- UF-05: Просмотр каталога (гость)
- UF-06: Поиск запчастей по автомобилю
- UF-07: Просмотр карточки товара
- UF-08: Создание объявления (продавец)
- UF-09: Редактирование и публикация объявления
- UF-10: Управление изображениями товара
- UF-11: Начало чата с продавцом
- UF-12: Обмен сообщениями в чате
- UF-13: Поиск по сообщениям
- UF-14: Добавление в избранное
- UF-15: Редактирование профиля продавца
- UF-16: Управление сессиями
- UF-17: Админ — модерация объявлений
- UF-18: Админ — управление справочниками (авто)
- UF-19: Админ — управление пользователями
- UF-20: Админ — дашборд и статистика
- UF-21: Текстовый поиск и автокомплит
- UF-22: Выбор города
- UF-23: Смена пароля
- UF-24: Бизнес-профиль
- UF-25: Публичная страница продавца
UF-01: Регистрация нового пользователя
Роль: Гость URL: /auth/registerПриоритет: Critical API: POST /auth/register — {email, password, display_name}
Шаги
- Открыть главную страницу
/ - Нажать кнопку "Регистрация" / "Войти" в хедере
- Перейти на форму регистрации
- Заполнить поля:
- Имя (display_name)
- Пароль
- Подтверждение пароля
- Нажать "Зарегистрироваться"
- Ожидание: Перенаправление в личный кабинет
/cabinet - Ожидание: Отображение уведомления о необходимости подтвердить email
Негативные сценарии
- Регистрация с уже существующим email → ошибка "Email уже зарегистрирован"
- Пустые обязательные поля → валидация на клиенте
- Слишком короткий пароль → ошибка валидации
- Несовпадающие пароли → ошибка на клиенте
Playwright assertions
typescript
await expect(page).toHaveURL(/\/cabinet/);
await expect(page.getByText(/подтвердить|верифик/i)).toBeVisible();UF-02: Авторизация существующего пользователя
Роль: Зарегистрированный пользователь URL: /auth/loginПриоритет: Critical API: POST /auth/login — {email, password}
Шаги
- Открыть
/auth/login - Ввести email
- Ввести пароль
- Нажать "Войти"
- Ожидание: Перенаправление в
/cabinet - Ожидание: В хедере отображается имя пользователя / аватар
Негативные сценарии
- Неверный пароль → ошибка "Неверный email или пароль"
- Несуществующий email → та же ошибка (не раскрывать существование аккаунта)
- 5 неудачных попыток → rate limit (прогрессивные задержки 2s→5s→10s, блокировка после 10)
Playwright assertions
typescript
await expect(page).toHaveURL(/\/cabinet/);
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();UF-03: Верификация email
Роль: Авторизованный пользователь (email не подтверждён) URL: /auth/verify-emailПриоритет: High API: POST /auth/verify-email — {code}, POST /auth/resend-verification
Шаги
- Войти в аккаунт с неподтверждённым email
- Увидеть баннер/уведомление "Подтвердите email"
- Проверить почту (для тестов — через API или mock)
- Ввести 6-значный код
- Нажать "Подтвердить"
- Ожидание: Баннер исчезает
- Ожидание: Доступна кнопка "Создать объявление"
Негативные сценарии
- Неверный код → ошибка
- Истёкший код (15 мин) → запросить повторно
- Нажать "Отправить повторно" → новый код (пауза 60 сек между запросами)
UF-04: Сброс пароля
Роль: Гость URL: /auth/forgot-password → /auth/reset-passwordПриоритет: High API: POST /auth/forgot-password — {email}, POST /auth/reset-password — {token, password}
Шаги
- На странице логина нажать "Забыли пароль?"
- Ввести email
- Нажать "Отправить ссылку"
- Перейти по ссылке из email → открывается
/auth/reset-password?token=... - Ввести новый пароль + подтверждение
- Нажать "Сбросить пароль"
- Ожидание: Перенаправление на логин с сообщением об успехе
- Войти с новым паролем
Негативные сценарии
- Несуществующий email → ответ всегда 200 (не раскрываем наличие аккаунта)
- Невалидный/истёкший токен → ошибка "Ссылка недействительна"
- Rate limit: 3 запроса/час на email
Важно: Backend использует token-based сброс (ссылка в email), НЕ 6-значный код. Токен передаётся через URL query параметр.
UF-05: Просмотр каталога (гость)
Роль: Гость URL: /catalogПриоритет: Critical API: GET /store/products — ?category_id&make_id&price_min&sort=date_desc&limit&cursor
Шаги
- Открыть страницу каталога
/catalog - Увидеть список объявлений (карточки товаров)
- Прокрутить вниз → подгрузка следующей страницы (cursor-based pagination)
- Проверить карточку товара содержит:
- Изображение (thumbnail)
- Название
- Цена
- Город/район
- Марка/модель авто (если указана)
- Нажать на карточку → переход на
/product/{id}
Playwright assertions
typescript
await expect(page.locator('[data-testid="product-card"]')).toHaveCount.greaterThan(0);
await page.locator('[data-testid="product-card"]').first().click();
await expect(page).toHaveURL(/\/product\/\d+/);UF-06: Поиск запчастей по автомобилю
Роль: Гость / Авторизованный URL: / (hero YMM фильтр) или /catalog (фильтры) Приоритет: High API: Каскадные GET-запросы (см. ниже)
Шаги
- Открыть главную страницу
- В фильтрах найти блок "Автомобиль"
- Выбрать марку (Make) из выпадающего списка → загружаются модели
- Выбрать модель (Model) → загружаются поколения
- Выбрать поколение (Generation) → загружаются модификации
- (Опционально) Выбрать модификацию
- Нажать "Найти" / применить фильтр
- Ожидание: Список обновляется, показывает только подходящие запчасти
- Очистить фильтры → возврат к полному каталогу
Каскадная зависимость (YMMM)
Марка → Модель → Поколение → Модификация
GET /store/cars/makes
→ GET /store/cars/makes/{id}/models
→ GET /store/cars/models/{id}/generations
→ GET /store/cars/generations/{id}/modificationsPlaywright assertions
typescript
// Каскад: после выбора марки появляются модели
await page.locator('[data-testid="make-select"]').selectOption('BMW');
await expect(page.locator('[data-testid="model-select"] option')).toHaveCount.greaterThan(1);UF-07: Просмотр карточки товара
Роль: Гость / Авторизованный URL: /product/{id}Приоритет: Critical API: GET /store/products/{id}
Шаги
- Перейти на страницу товара
- Проверить отображение:
- Галерея изображений (слайдер/лайтбокс)
- Название товара
- Цена
- Описание
- OEM-номер (если есть)
- Производитель
- Категории (breadcrumbs)
- Совместимость (марка/модель/поколение авто)
- Местоположение (город, район, метро)
- Признак "Комплект" (is_kit)
- Проверить блок продавца:
- Имя
- Аватар
- Рейтинг и кол-во отзывов
- Кнопка "Написать продавцу"
- Нажать "Написать продавцу":
- Если гость → редирект на
/auth/login - Если авторизован → создаётся conversation (POST /vendor/conversations) → редирект в
/cabinet/messages/{id}
- Если гость → редирект на
- Нажать на изображение → открытие лайтбокса
- Переключение между фото в лайтбоксе
Тест-кейсы (DEV-222)
| Кейс | Действие | Ожидание |
|---|---|---|
| Название | Открыть карточку | product-title видим, не пуст |
| Цена | Открыть карточку | product-price содержит цифры |
| Описание | Открыть карточку | product-description видим (skip если нет) |
| Блок продавца | Открыть карточку | seller-card + ссылка на /seller/{id} |
| Галерея | Открыть карточку | product-gallery с img или placeholder |
| Миниатюры | Клик на 2-ю миниатюру | border-primary класс (skip если <2 фото) |
| Характеристики | Открыть seed-товар | Заголовок «Характеристики», ≥1 строка (OEM/manufacturer/steering/category) |
| Совместимость | Открыть seed-товар | Заголовок «Подходит для», ≥1 строка с маркой |
| Контакт | Открыть карточку гостем | contact-seller кнопка видна |
Тесты характеристик и совместимости обязательны при наличии seed-товара, graceful skip для fallback.
UF-08: Создание объявления (продавец)
Роль: Авторизованный продавец (email подтверждён) URL: /cabinet/products/newПриоритет: Critical API: POST /vendor/products, POST /vendor/products/{id}/images
Шаги
- Перейти в личный кабинет
/cabinet - Нажать "Создать объявление"
- Заполнить форму (порядок полей — см. DEV-287/290/291):
- Название товара (DEV-301: макс. 50 символов, виден счётчик)
- Чип смарт-подсказки категории под полем названия — появляется после debounce 300ms (DEV-317,
GET /store/categories/suggest). Клик → категория подставляется; крестик отклоняет предложение. Чип показывается даже если категория уже выбрана — как альтернативный вариант. - Цена (DEV-311: макс. 200 000 000; под полем — подсказка средней цены по категории из
GET /store/categories/{id}/price-stats, DEV-319) - Описание
- OEM-номера (DEV-309: клиентская валидация формата — только
A-Z0-9, длина ограничена; один или несколько) - Производитель
- Тип руля (левый/правый/универсальный)
- Категория (минимум 1, N-уровневый каскад; в форме товара пункт «Все»/«Не выбрано» недоступен — DEV-284; в фильтрах каталога сохраняется через
clearableпропс) - Категория состояния (condition)
- Местоположение: регион → город → район → метро
- Совместимость авто (YMM): марка → модель → поколение → модификация. Первая строка добавляется автоматически и не может быть удалена (DEV-292). Поле "марка" валидируется. Есть тумблер "Неизвестный автомобиль" (DEV-293/307) — при включении блок YMM отключается, товар сохраняется без привязки к авто.
- Признак "Комплект" (is_kit)
- Телефон для связи (phone_id)
- Загрузить фото (мин. 1, макс. 10 — см. UF-10)
- Нажать "Сохранить черновик"
- Ожидание: Объявление создано со статусом
draft - Ожидание: Перенаправление на страницу объявления или список
Негативные сценарии
- Без заголовка → валидация
- Название > 50 символов → обрезка / блокировка ввода (DEV-301)
- OEM в неверном формате → ошибка валидации (DEV-309)
- Цена > 200 000 000 → клампится к максимуму (DEV-311)
- Без категории → ошибка "Выберите хотя бы 1 категорию"
- Без фото → предупреждение
- YMM: попытка удалить последнюю строку → кнопка удаления недоступна (DEV-292)
- Email не верифицирован → нельзя опубликовать (только черновик)
Playwright assertions
typescript
await page.fill('[data-testid="product-title"]', 'Тормозные колодки BMW');
await page.fill('[data-testid="product-price"]', '3500');
// ... заполнение остальных полей
await page.click('[data-testid="save-draft"]');
await expect(page.getByText(/черновик|draft/i)).toBeVisible();UF-09: Редактирование и публикация объявления
Роль: Авторизованный продавец URL: /cabinet/products/{id}/editПриоритет: Critical API: PUT /vendor/products/{id}, POST /vendor/products/{id}/publish
Шаги
- Перейти в "Мои объявления"
/cabinet/products - Найти черновик
- Нажать "Редактировать"
- Изменить поля (название, цена, описание)
- Нажать "Сохранить"
- Ожидание: Данные обновлены
- Нажать "Опубликовать"
- Ожидание: Статус меняется на
pending(на модерации) - Ожидание: Отображается уведомление "Объявление отправлено на модерацию"
Статусы объявления
draft → pending → active (одобрено) / rejected (отклонено)
active → sold / archivedРедактирование доступно для
draft,rejected, а такжеpending(DEV-313). При редактировании товара в статусеpendingпоказывается предупреждение (UAlert, soft tone в светлой теме) о том, что после сохранения товар снова попадёт на модерацию. Публикация — только изdraft.
UF-10: Управление изображениями товара
Роль: Авторизованный продавец URL: /cabinet/products/{id}/editПриоритет: Medium API: POST /vendor/products/{id}/images, PUT /vendor/products/{id}/images/order, DELETE /vendor/products/{id}/images/{imgId}
Шаги
- Открыть редактирование объявления
- Загрузить новое изображение (кнопка загрузки, макс. 10МБ, макс. 10 фото на товар — DEV-303)
- Ожидание: Изображение появляется в галерее
- Перетащить фото для изменения порядка (drag-and-drop — DEV-305)
- Ожидание: Порядок сохранён (PUT
/vendor/products/{id}/images/orderс массивом image_ids) - Удалить изображение → нажать крестик
- Ожидание: Изображение удалено; если удалено главное фото — главным автоматически становится следующее по порядку (DEV-303)
Edge-cases и UX-детали (DEV-303, DEV-305)
- Достигнут лимит 10 фото → кнопка добавления скрывается, под галереей появляется хинт с текущим лимитом
- Ошибка загрузки фото → рядом с превью отображаются иконка ошибки и кнопка повтора (retry)
- Ни одно фото не помечено как основное → фолбэк: основным становится первое в порядке
- URL-эндпоинты vendor-товаров вызываются через фабрику
app/entities/product/api/endpoints.ts→vendorProductEndpoints(DEV-305) — никаких захардкоженных строк в composables/features/pages/tests; тесты импортируют ту же фабрику
UF-11: Начало чата с продавцом
Роль: Авторизованный покупатель URL: /product/{id} → /cabinet/messages/{conversationId}Приоритет: High API: POST /vendor/conversations — {product_id, seller_id}, POST /vendor/conversations/{id}/messages — {text}
Шаги
- Открыть карточку товара
- Нажать "Написать продавцу"
- Ожидание: Создаётся conversation (POST /vendor/conversations)
- Ожидание: Перенаправление в чат
/cabinet/messages/{id} - Ввести сообщение "Здравствуйте, товар ещё в наличии?"
- Нажать отправить
- Ожидание: Сообщение появляется в чате
- Ожидание: У продавца — уведомление о новом сообщении
Backend API (готов)
POST /vendor/conversations — создание беседы
GET /vendor/conversations — список бесед
GET /vendor/conversations/unread-count — счётчик непрочитанных
GET /vendor/conversations/{id} — детали беседы
DELETE /vendor/conversations/{id} — удаление беседы
POST /vendor/conversations/{id}/messages — отправка текста
POST /vendor/conversations/{id}/messages/image — отправка изображения
POST /vendor/conversations/{id}/read — отметить прочитанным
POST /vendor/conversations/{id}/delivered — отметить доставленным
POST /vendor/conversations/{id}/typing — индикатор набора
GET /vendor/conversations/{id}/messages — история сообщений
GET /vendor/users/{id}/online — онлайн-статусPlaywright assertions
typescript
await page.click('[data-testid="contact-seller"]');
await expect(page).toHaveURL(/\/cabinet\/messages\/\d+/);
await page.fill('[data-testid="message-input"]', 'Здравствуйте!');
await page.click('[data-testid="send-message"]');
await expect(page.locator('.message-bubble').last()).toContainText('Здравствуйте!');UF-12: Обмен сообщениями в чате
Роль: Покупатель и Продавец URL: /cabinet/messages/{id}Приоритет: High API: см. UF-11 + Centrifugo WebSocket (POST /centrifugo/connect, POST /centrifugo/disconnect)
Шаги
- Покупатель отправляет текстовое сообщение
- Продавец видит сообщение в реальном времени (Centrifugo WebSocket)
- Продавец отвечает
- Покупатель видит ответ в реальном времени
- Проверить индикатор "печатает..." (typing indicator через POST /conversations/{id}/typing)
- Отправить изображение через чат (POST /conversations/{id}/messages/image)
- Ожидание: Изображение загружается и отображается как thumbnail
- Нажать на изображение → открывается лайтбокс
Для тестирования real-time
Требуется 2 сессии браузера (2 контекста Playwright) — покупатель и продавец.
typescript
// Контекст 1: Покупатель
const buyerPage = await buyerContext.newPage();
await buyerPage.goto(`/cabinet/messages/${conversationId}`);
// Контекст 2: Продавец
const sellerPage = await sellerContext.newPage();
await sellerPage.goto(`/cabinet/messages/${conversationId}`);
// Покупатель пишет
await buyerPage.fill('[data-testid="message-input"]', 'Привет!');
await buyerPage.click('[data-testid="send-message"]');
// Продавец видит
await expect(sellerPage.locator('.message-bubble').last()).toContainText('Привет!');UF-13: Поиск по сообщениям
Роль: Авторизованный пользователь URL: /cabinet/messages, /cabinet/messages/{id}Приоритет: Medium API: GET /vendor/conversations/{id}/messages?search=, GET /vendor/conversations?search=
Шаги
- Перейти в список сообщений
/cabinet/messages - Ввести запрос в поле глобального поиска (минимум 2 символа)
- Ожидание: Появляются результаты (сгруппированные по беседам)
- Нажать на результат → переход в беседу с подсветкой найденного сообщения
- Проверить highlight сообщения (оранжевая рамка)
- Поиск внутри конкретной беседы (ChatSearchBar)
- Навигация между найденными сообщениями (стрелки вверх/вниз)
UF-14: Добавление в избранное
Роль: Авторизованный пользователь URL: /product/{id} → /cabinet/favoritesПриоритет: Medium API: POST /vendor/favorites — {product_id}, DELETE /vendor/favorites/{product_id}, GET /vendor/favorites
Шаги
- Открыть карточку товара
- Нажать кнопку "В избранное" (сердечко/звезда)
- Ожидание: Кнопка меняет состояние (заполненная иконка)
- Перейти в
/cabinet/favorites - Ожидание: Товар отображается в списке
- Нажать "Удалить из избранного"
- Ожидание: Товар исчезает из списка
Playwright assertions
typescript
await page.click('[data-testid="favorite-toggle"]');
await expect(page.locator('[data-testid="favorite-toggle"]')).toHaveAttribute('data-active', 'true');
await page.goto('/cabinet/favorites');
await expect(page.locator('[data-testid="product-card"]')).toHaveCount(1);UF-15: Редактирование профиля продавца
Роль: Авторизованный пользователь URL: /cabinet/settings/profileПриоритет: Medium API: GET /vendor/me, PUT /vendor/me, POST /vendor/me/avatar
Шаги
- Перейти в
/cabinet/settings/profile - Изменить:
- Имя (display_name)
- Аватар (загрузить файл, макс. 5МБ)
- Город → район → метро (geo cascade)
- Тип аккаунта (личный / бизнес)
- Нажать "Сохранить"
- Ожидание: Данные обновлены
- Ожидание: Аватар обновлён в хедере
Примечание: Управление телефонами реализовано на frontend (PhoneManager). Телефон сохраняется как поле профиля через
PUT /vendor/me.
UF-16: Управление сессиями
Роль: Авторизованный пользователь URL: /cabinet/settings/securityПриоритет: Low API: GET /vendor/sessions, DELETE /vendor/sessions/{id}, POST /auth/logout-all
Шаги
- Перейти в настройки безопасности
/cabinet/settings/security - Увидеть список активных сессий (IP, устройство, дата)
- Текущая сессия помечена
- Нажать "Завершить" рядом с другой сессией
- Ожидание: Сессия удалена из списка
- Нажать "Завершить все сессии" (POST /auth/logout-all)
- Ожидание: Все сессии кроме текущей удалены
UF-17: Админ — модерация объявлений
Роль: Администратор URL: /admin/productsПриоритет: Critical API: GET /admin/products, GET /admin/products/pending, GET /admin/products/{id}, PUT /admin/products/{id}/approve, PUT /admin/products/{id}/reject
Шаги
- Войти как администратор
- Перейти в
/admin/products - Увидеть список объявлений на модерации (status = pending)
- Нажать на объявление → просмотр деталей (GET /admin/products/{id})
- Проверить: фото, название, цена, описание, совместимость
- Сценарий A: Одобрить
- Нажать "Одобрить"
- Ожидание: Статус →
active - Ожидание: Продавец получает уведомление
- Сценарий B: Отклонить
- Нажать "Отклонить"
- Ввести причину отклонения (
{reason: string}) - Нажать "Подтвердить"
- Ожидание: Статус →
rejected - Ожидание: Продавец получает уведомление с причиной
Playwright assertions
typescript
await page.goto('/admin/products');
await page.click('[data-testid="product-row"]:first-child');
await page.click('[data-testid="approve-btn"]');
await expect(page.getByText(/active|одобрено/i)).toBeVisible();UF-18: Админ — управление справочниками (авто)
Роль: Администратор URL: /admin/referencesПриоритет: High API: CRUD через /admin/cars/*, /admin/categories, /admin/geo/*
Шаги
- Перейти в раздел справочников
- Марки:
- Список всех марок
- Добавить марку (имя, slug, is_popular)
- Редактировать марку
- Удалить марку (409 если есть дочерние модели или привязанные товары)
- Модели:
- Выбрать марку → список моделей
- Добавить модель
- Редактировать модель
- Удалить модель (409 если есть дочерние поколения или товары)
- Поколения:
- Выбрать модель → список поколений
- Добавить поколение (год от, руль)
- Удалить поколение (409 если есть дочерние модификации или товары)
- Модификации:
- Выбрать поколение → список модификаций
- Добавить модификацию (тип топлива, привод, КПП)
- Каскадная защита (DEV-221):
- Попытка удалить запись с дочерними → бэкенд возвращает 409 Conflict
- Фронтенд показывает toast с текстом ошибки от бэкенда
- Запись остаётся в списке (не удалена)
Тест-кейсы
| Кейс | Действие | Ожидание |
|---|---|---|
| CRUD марки | Создать → редактировать → удалить | Успешные toast-ы |
| CRUD модели | Выбрать марку → создать модель → редактировать | Модель в списке |
| CRUD поколения | Выбрать модель → создать поколение | Поколение в списке |
| CRUD модификации | Выбрать поколение → создать модификацию | Модификация в списке |
| 409 при удалении | Удалить марку с моделями | Toast с сообщением от бэкенда, марка остаётся (test.fixme — ждёт бэкенд DEV-219) |
Data-testid
| Элемент | testid |
|---|---|
| Колонка списка | reference-list-{makes|models|generations|modifications} |
| Кнопка добавления | ref-add-btn |
| Элемент списка | ref-item-{id} |
| Кнопка редактирования | ref-edit-{id} |
| Кнопка удаления | ref-delete-{id} |
| Подтверждение удаления | ref-delete-confirm |
| Поле формы | ref-field-{key} |
| Сабмит формы | ref-form-submit |
UF-19: Админ — управление пользователями
Роль: Администратор URL: /admin/usersПриоритет: Medium API: GET /admin/users, GET /admin/users/{id}, PUT /admin/users/{id}, DELETE /admin/users/{id}
Шаги
- Перейти в
/admin/users - Увидеть таблицу пользователей с фильтрами
- Фильтровать по типу аккаунта (personal/business)
- Нажать на пользователя → детали (контакты, аккаунт, статистика)
- Заблокировать пользователя → подтвердить → кнопка меняется на «Разблокировать»
- Разблокировать обратно → восстановить исходное состояние
Тест-кейсы (DEV-222)
| Кейс | Действие | Ожидание |
|---|---|---|
| Таблица | Открыть /admin/users | Таблица видна, ≥1 строка |
| Фильтр | Выбрать «Бизнес» | API запрос с account_type=business, 200 OK |
| Детали | Клик на строку | URL /admin/users/{id}, секции контактов/аккаунта/статистики |
| Блокировка | Клик «Заблокировать» → подтвердить | PUT 200, кнопка → «Разблокировать» |
| Разблокировка | Клик «Разблокировать» → подтвердить | PUT 200, кнопка → «Заблокировать» |
Тест блокировки выбирает не-админского пользователя и восстанавливает исходное состояние после проверки.
UF-20: Админ — дашборд и статистика
Роль: Администратор URL: /adminПриоритет: Medium API: GET /admin/stats, GET /admin/stats/products, GET /admin/stats/users
Шаги
- Войти как админ → автоматически
/admin - Проверить 4 карточки статистики:
- Всего пользователей (+ новых сегодня)
- Всего объявлений (+ новых сегодня)
- На модерации (pending)
- Черновики (draft)
- Нажать "На модерации" → переход к
/admin/products - Проверить блок "Пользователи" (по типу аккаунта)
- Проверить блок "Объявления" (по статусу)
Playwright assertions
typescript
await page.goto('/admin');
await expect(page.locator('[data-testid="stat-card"]')).toHaveCount(4);
await expect(page.getByText(/всего пользователей/i)).toBeVisible();UF-21: Текстовый поиск и автокомплит
Роль: Гость / Авторизованный URL: / (хедер), /catalog?q=...Приоритет: High API: GET /store/products/suggest?q= — автокомплит, GET /store/products?q= — результаты
Шаги
- Кликнуть на поле поиска в хедере
- Начать вводить запрос (минимум 2 символа)
- Ожидание: Появляется выпадающий список саджестов (ProductSuggestion)
- Каждый саджест содержит:
- Название товара
- Цена
- Thumbnail изображение
- Нажать на саджест → переход на
/product/{id} - Или: нажать Enter / "Найти все" → переход на
/catalog?q={запрос} - Ожидание: Каталог отфильтрован по запросу, доступна сортировка по релевантности
Негативные сценарии
- Пустой запрос → саджесты не показываются
- Менее 2 символов → нет запроса к API
- Нет результатов → сообщение "Ничего не найдено"
Playwright assertions
typescript
await page.locator('[data-testid="search-input"]').fill('колодки');
await expect(page.locator('[data-testid="search-suggestion"]')).toHaveCount.greaterThan(0);
await page.locator('[data-testid="search-suggestion"]').first().click();
await expect(page).toHaveURL(/\/product\/\d+/);UF-22: Выбор города
Роль: Гость / Авторизованный URL: любая страница (хедер) Приоритет: Medium API: GET /store/geo/regions, GET /store/geo/regions/{id}/cities
Шаги
- Нажать на индикатор города в хедере
- Ожидание: Открывается модальное окно выбора города
- Увидеть список регионов
- Выбрать регион → загружаются города
- Выбрать город
- Ожидание: Модалка закрывается
- Ожидание: Индикатор в хедере обновлён
- Ожидание: Город сохранён в cookie (персистенция между сессиями)
- Перейти в каталог → товары отфильтрованы по выбранному городу
Playwright assertions
typescript
await page.locator('[data-testid="city-selector"]').click();
await expect(page.locator('[data-testid="city-modal"]')).toBeVisible();
await page.getByText('Санкт-Петербург').click();
await expect(page.locator('[data-testid="city-selector"]')).toContainText('Санкт-Петербург');UF-23: Смена пароля
Роль: Авторизованный пользователь URL: /cabinet/settings/securityПриоритет: Medium API: PUT /vendor/me (поле password)
Шаги
- Перейти в
/cabinet/settings/security - Заполнить:
- Текущий пароль
- Новый пароль
- Подтверждение нового пароля
- Нажать "Сменить пароль"
- Ожидание: Успешное уведомление
- Выйти и войти с новым паролем
Негативные сценарии
- Неверный текущий пароль → ошибка
- Новый пароль не соответствует требованиям → валидация
- Пароли не совпадают → ошибка на клиенте
UF-24: Бизнес-профиль
Роль: Авторизованный пользователь (бизнес-аккаунт) URL: /cabinet/settings/businessПриоритет: Low API: PUT /vendor/me (поля business profile)
Шаги
- Перейти в
/cabinet/settings/profile - Переключить тип аккаунта на "Бизнес"
- Перейти на вкладку "Бизнес-профиль" (
/cabinet/settings/business) - Заполнить:
- Название компании
- ИНН
- Адрес
- Сайт
- Часы работы
- Нажать "Сохранить"
- Ожидание: Данные обновлены
- Открыть публичный профиль продавца → отображается бизнес-информация
UF-25: Публичная страница продавца
Роль: Гость / Авторизованный URL: /seller/{id}Приоритет: Medium API: GET /store/sellers/{id}, GET /store/sellers/{id}/products
Шаги
- Перейти на
/seller/{id}(через ссылку из карточки товара) - Проверить отображение:
- Аватар продавца
- Имя / название компании
- Рейтинг и количество отзывов
- Город / район
- Дата регистрации
- Количество объявлений
- Проверить список товаров продавца (сетка карточек)
- Пагинация (cursor-based) при прокрутке
- Нажать на карточку товара → переход на
/product/{id} - Нажать "Написать продавцу":
- Если гость → редирект на
/auth/login - Если авторизован → создание беседы →
/cabinet/messages/{id}
- Если гость → редирект на
Playwright assertions
typescript
await page.goto('/seller/1');
await expect(page.locator('[data-testid="seller-name"]')).toBeVisible();
await expect(page.locator('[data-testid="product-card"]')).toHaveCount.greaterThan(0);Тестовые данные
Пользователи
| Роль | Пароль | Примечания | |
|---|---|---|---|
| Покупатель | buyer@test.partizap.ru | Test123!pass | email verified |
| Продавец | seller@test.partizap.ru | Test123!pass | email verified, есть объявления |
| Админ | admin@test.partizap.ru | Admin123!pass | is_admin=true |
| Новый | newuser@test.partizap.ru | New123!pass | email НЕ verified |
Справочные данные
- Марки: BMW, Toyota, Mercedes-Benz (из seeder'а)
- Города: Санкт-Петербург, Москва
- Категории: по типам part/condition/attribute
Тестовая инфраструктура
Playwright проекты и зависимости
gate → setup → smoke
gate → functional| Проект | Описание | storageState |
|---|---|---|
gate | Проход формы авторизации dev-сервера (gate.setup.ts) | — (создаёт GATE_STATE) |
setup | Логин ролей через API (auth.setup.ts) | GATE_STATE |
smoke | Быстрые проверки доступности (tests/e2e/smoke/) | GATE_STATE |
functional | Функциональные тесты по user flows (tests/e2e/{auth,catalog,...}/) | GATE_STATE |
Dev server auth gate
Dev-сервер (dev.partizap.ru) защищён формой авторизации (POST /auth/verify), которая показывается вместо приложения. Это не HTTP Basic Auth — httpCredentials Playwright не обходит эту форму для browser navigation.
Решение:
gate.setup.tsнавигирует на dev-сервер, заполняет форму, сабмитит → сохраняет cookies вGATE_STATE- Все проекты наследуют
storageState: GATE_STATE— форма больше не показывается httpCredentialsв конфиге всё ещё нужен дляrequestfixture (прямые API-вызовы в smoke-тестах)
Обработка 429 Rate Limit
API rate limiter считает попытки по IP за временное окно. В E2E-тестах многократные login/register вызывают 429.
Паттерны обработки:
- Негативные тесты:
.or()— принимаем либо ожидаемую ошибку, либо rate limit:typescriptawait expect( page.getByText(/неверный email или пароль/i) .or(page.getByText(/слишком много попыток/i)), ).toBeVisible() - Позитивные тесты:
waitFor+test.skip— если rate limit, пропускаем тест:typescriptconst rateLimited = await page.getByText(/слишком много попыток/i) .waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) if (rateLimited) test.skip(true, 'Rate limited — retry after cooldown') - Setup (loginViaApi):
RateLimitError→setup.skip()— пропускает зависимые тесты - CSRF/валидация ошибки: frontend отображает CSRF-ошибки как "Ошибка валидации". Тесты добавляют этот текст в
.or()паттерны:typescriptpage.getByText(/неверный email или пароль/i) .or(page.getByText(/слишком много попыток/i)) .or(page.getByText(/ошибка валидации/i))
CSRF cookie и page.request vs request fixture
PHP backend устанавливает CSRF_TOKEN cookie через Set-Cookie заголовок на API-запросы. В Playwright есть два API-контекста:
requestfixture — отдельный HTTP-клиент со своим cookie jar. НЕ делится cookies с browser contextpage.request— API-клиент, который разделяет cookie jar с browser context страницы
Проблема: если получить CSRF cookie через request.get('/api/health'), он не попадёт в browser context. При отправке формы через UI, frontend не найдёт CSRF_TOKEN в document.cookie → 422 ошибка.
Решение: всегда использовать page.request для установки CSRF cookie перед работой с формами:
typescript
// ✅ Правильно — cookie попадает в browser context
const baseURL = process.env.E2E_BASE_URL || 'https://dev.partizap.ru'
await page.request.get(`${baseURL}/api/health`)
// ❌ Неправильно — cookie изолирован в отдельном контексте
await request.get(`${baseURL}/api/health`)loginViaApi() в auth.helper.ts также использует page.request для login-запроса, чтобы session cookie (PARTIZAP_SESSION) автоматически попал в browser context.
Nuxt SSR + гидратация
Nuxt SSR рендерит HTML, но Vue event handlers привязываются только после гидратации. Если Playwright кликает кнопку до гидратации, происходит native form GET submit вместо SPA-обработки.
Решение: waitUntil: 'networkidle' на page.goto() для тестов с интерактивными формами:
typescript
await page.goto('/auth/login', { waitUntil: 'networkidle' })Nuxt UI v3 — селекторы ошибок валидации
UFormField рендерит ошибки с атрибутом data-slot="error" (не CSS-класс .text-error):
typescript
await expect(
page.locator('[data-testid="login-form"] [data-slot="error"]').first(),
).toBeVisible()Тестовые пароли и Zod-валидация
passwordSchema отклоняет последовательные символы (abc, 123, 456). Тестовые пароли должны избегать таких паттернов:
- Корректно:
Str0ng!Pass,D1fferent!Pass - Некорректно:
StrongPass123!(содержит123)
npm-команды
bash
npm run test:e2e # Все проекты (gate + setup + smoke + functional)
npm run test:e2e:fn # Только gate + functional (без smoke)
npm run test:e2e:ui # Playwright UI modeCI/CD переменные
| Variable | Назначение | Используется в |
|---|---|---|
E2E_BASE_URL | URL стенда (default: https://dev.partizap.ru) | playwright.config.ts |
E2E_HTTP_USER | Логин формы dev gate | gate.setup.ts, httpCredentials |
E2E_HTTP_PASSWORD | Пароль формы dev gate | gate.setup.ts, httpCredentials |
E2E_USER_EMAIL | Email тестового покупателя | auth.setup.ts, functional tests |
E2E_USER_PASSWORD | Пароль тестового покупателя | auth.setup.ts, functional tests |
E2E_SELLER_EMAIL | Email тестового продавца | auth.setup.ts |
E2E_SELLER_PASSWORD | Пароль тестового продавца | auth.setup.ts |
E2E_ADMIN_EMAIL | Email тестового админа | auth.setup.ts |
E2E_ADMIN_PASSWORD | Пароль тестового админа | auth.setup.ts |
Для локального запуска — файл .env.e2e (загружается в playwright.config.ts). В CI — GitLab CI/CD Variables (masked, not protected).
Порядок выполнения для записи тестов
Фаза 1: Базовые flows (без авторизации)
- UF-05: Каталог
- UF-07: Карточка товара
- UF-06: Поиск/фильтры
Фаза 2: Авторизация
- UF-01: Регистрация
- UF-02: Логин
- UF-03: Верификация email
- UF-04: Сброс пароля
Фаза 3: Продавец
- UF-08: Создание объявления
- UF-10: Изображения
- UF-09: Редактирование и публикация
- UF-15: Профиль
- UF-14: Избранное
Фаза 4: Чат
- UF-11: Начало чата
- UF-12: Обмен сообщениями
- UF-13: Поиск по сообщениям
Фаза 5: Админка
- UF-20: Дашборд
- UF-17: Модерация
- UF-18: Справочники
- UF-19: Пользователи
- UF-16: Сессии
Фаза 6: Дополнительные flows
- UF-21: Текстовый поиск и автокомплит
- UF-22: Выбор города
- UF-23: Смена пароля
- UF-24: Бизнес-профиль
- UF-25: Публичная страница продавца
Как записывать тесты
Через Gasoline MCP (рекомендуется)
- Запустить приложение локально (frontend dev server + backend)
- Открыть нужную страницу в Chrome с Gasoline extension
- Прокликать user flow вручную
- Gasoline записывает все действия (клики, ввод, навигацию)
- Сгенерировать Playwright-тест:
gasoline generate --what=test --test_name="UF-01 Registration" - Доработать сгенерированный тест (добавить assertions, data-testid)
Через Playwright Codegen
bash
npx playwright codegen http://localhost:3000Через gasoline reproduce
gasoline generate --what=reproduction --last_n=20 --include_screenshots=trueСтруктура файлов тестов
tests/e2e/
├── gate.setup.ts # Проход формы авторизации dev-сервера
├── auth.setup.ts # Логин ролей (user, seller, admin) через API
├── smoke/ # Smoke-тесты (быстрые проверки доступности)
│ ├── auth.spec.ts
│ ├── catalog.spec.ts
│ ├── create-product.spec.ts
│ ├── moderation.spec.ts
│ └── ymm-search.spec.ts
├── auth/ # ✅ Функциональные тесты авторизации (DEV-204)
│ ├── login.spec.ts # UF-02: 6 тестов
│ ├── registration.spec.ts # UF-01: 4 теста
│ └── password-reset.spec.ts # UF-04: 3 теста
├── catalog/ # ✅ Функциональные тесты каталога (DEV-205)
│ ├── catalog.spec.ts # UF-05: 5 тестов (карточки, пагинация, сортировка)
│ ├── product-detail.spec.ts # UF-07: 9 тестов (название, цена, описание, галерея, характеристики, совместимость, контакты)
│ ├── ymm-cascade.spec.ts # UF-06: 4 теста (каскад марка→модель, фильтр, сброс)
│ └── search.spec.ts # UF-21: 8 тестов (автокомплит, поиск, пустой результат)
├── seller/ # ✅ DEV-206
│ └── create-product.spec.ts # UF-08/09/10: 7 тестов (создание, валидация, фото, публикация)
├── chat/ # ✅ DEV-207
│ ├── start-conversation.spec.ts # UF-11: 2 теста (клик «Написать продавцу» → редирект в чат)
│ └── messaging.spec.ts # UF-12: 3 теста (создание беседы через API → отправка сообщения → cleanup)
├── admin/ # ⚠️ Отключены (DEV-300) — admin API вынесен на `dev-admin.partizap.ru`
│ └── moderation.spec.ts # UF-17: исключён из `functional` и `smoke` проектов в playwright.config.ts; seed approve step — best-effort
├── helpers/
│ ├── auth.helper.ts # passDevGate(), loginViaApi(), RateLimitError, getTestUser/Seller/Admin
│ ├── auth.state.ts # Пути к storageState файлам (GATE/USER/SELLER/ADMIN_STATE)
│ └── api.helper.ts # Прямые API-вызовы для сетапа
└── .auth/ # Автогенерируемые storageState файлы (в .gitignore)
├── gate.json
├── user.json
├── seller.json
└── admin.jsonBackend API — полный реестр эндпоинтов (79 шт.)
Для сверки с тестами. Сгруппировано по группам маршрутов.
Auth (9)
| Method | Endpoint | Rate Limit |
|---|---|---|
| POST | /auth/login | 5/15мин на email |
| POST | /auth/register | 3/час на IP |
| POST | /auth/forgot-password | 3/час на email |
| POST | /auth/reset-password | — |
| POST | /auth/logout | auth |
| POST | /auth/logout-all | auth |
| GET | /auth/me | auth |
| POST | /auth/verify-email | 5/15мин на user, auth |
| POST | /auth/resend-verification | auth |
Store — публичные (12)
| Method | Endpoint |
|---|---|
| GET | /store/products |
| GET | /store/products/search |
| GET | /store/products/{id} |
| GET | /store/sellers/{id} |
| GET | /store/sellers/{id}/products |
| GET | /store/categories |
| GET | /store/cars/makes |
| GET | /store/cars/makes/{id}/models |
| GET | /store/cars/models/{id}/generations |
| GET | /store/cars/generations/{id}/modifications |
| GET | /store/geo/regions |
| GET | /store/geo/regions/{id}/cities |
| GET | /store/geo/cities/{id}/districts |
| GET | /store/geo/cities/{id}/metro |
Vendor — авторизованные (26)
| Method | Endpoint | Описание |
|---|---|---|
| GET | /vendor/me | Профиль |
| PUT | /vendor/me | Обновление профиля |
| POST | /vendor/me/avatar | Аватар (5МБ) |
| GET | /vendor/products | Мои товары |
| POST | /vendor/products | Создание |
| GET | /vendor/products/{id} | Детали |
| PUT | /vendor/products/{id} | Обновление |
| DELETE | /vendor/products/{id} | Удаление |
| POST | /vendor/products/{id}/publish | Публикация |
| POST | /vendor/products/{id}/images | Загрузка фото (10МБ) |
| PUT | /vendor/products/{id}/images/order | Порядок фото |
| DELETE | /vendor/products/{id}/images/{imgId} | Удаление фото |
| GET | /vendor/conversations | Список бесед |
| POST | /vendor/conversations | Создание беседы |
| GET | /vendor/conversations/unread-count | Непрочитанные |
| GET | /vendor/conversations/{id} | Детали беседы |
| DELETE | /vendor/conversations/{id} | Удаление беседы |
| GET | /vendor/conversations/{id}/messages | История |
| POST | /vendor/conversations/{id}/messages | Текст |
| POST | /vendor/conversations/{id}/messages/image | Изображение |
| POST | /vendor/conversations/{id}/read | Прочитано |
| POST | /vendor/conversations/{id}/delivered | Доставлено |
| POST | /vendor/conversations/{id}/typing | Печатает |
| GET | /vendor/users/{id}/online | Онлайн-статус |
| GET | /vendor/favorites | Избранное |
| POST | /vendor/favorites | Добавить |
| DELETE | /vendor/favorites/{product_id} | Удалить |
| GET | /vendor/sessions | Сессии |
| DELETE | /vendor/sessions/{id} | Завершить сессию |
Admin (27)
| Method | Endpoint | Описание |
|---|---|---|
| GET | /admin/stats | Дашборд |
| GET | /admin/stats/products | Стат. товаров |
| GET | /admin/stats/users | Стат. юзеров |
| GET | /admin/users | Список |
| GET | /admin/users/{id} | Профиль |
| PUT | /admin/users/{id} | Обновление |
| DELETE | /admin/users/{id} | Удаление |
| GET | /admin/products | Все товары |
| GET | /admin/products/pending | На модерации |
| GET | /admin/products/{id} | Детали |
| PUT | /admin/products/{id}/approve | Одобрить |
| PUT | /admin/products/{id}/reject | Отклонить |
| DELETE | /admin/products/{id} | Удалить |
| POST | /admin/cars/makes | + марка |
| PUT | /admin/cars/makes/{id} | ~ марка |
| DELETE | /admin/cars/makes/{id} | - марка |
| POST | /admin/cars/models | + модель |
| PUT | /admin/cars/models/{id} | ~ модель |
| DELETE | /admin/cars/models/{id} | - модель |
| POST | /admin/cars/generations | + поколение |
| PUT | /admin/cars/generations/{id} | ~ поколение |
| DELETE | /admin/cars/generations/{id} | - поколение |
| POST | /admin/cars/modifications | + модификация |
| PUT | /admin/cars/modifications/{id} | ~ модификация |
| DELETE | /admin/cars/modifications/{id} | - модификация |
| POST | /admin/categories | + категория |
| PUT | /admin/categories/{id} | ~ категория |
| DELETE | /admin/categories/{id} | - категория |
| POST | /admin/geo/regions | + регион |
| PUT | /admin/geo/regions/{id} | ~ регион |
| POST | /admin/geo/cities | + город |
| PUT | /admin/geo/cities/{id} | ~ город |
| POST | /admin/geo/districts | + район |
| PUT | /admin/geo/districts/{id} | ~ район |
| POST | /admin/geo/metro-stations | + метро |
| PUT | /admin/geo/metro-stations/{id} | ~ метро |
System (3)
| Method | Endpoint | Описание |
|---|---|---|
| GET | /health | Health check |
| POST | /centrifugo/connect | WS proxy |
| POST | /centrifugo/disconnect | WS proxy |