Skip to content

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ПриоритетBackendFrontendГотов к тестуE2E автотест
UF-01 РегистрацияCritical✅ DEV-204
UF-02 ЛогинCritical✅ DEV-204
UF-03 Верификация emailHigh
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: Регистрация нового пользователя

Роль: Гость URL: /auth/registerПриоритет: Critical API: POST /auth/register{email, password, display_name}

Шаги

  1. Открыть главную страницу /
  2. Нажать кнопку "Регистрация" / "Войти" в хедере
  3. Перейти на форму регистрации
  4. Заполнить поля:
    • Имя (display_name)
    • Email
    • Пароль
    • Подтверждение пароля
  5. Нажать "Зарегистрироваться"
  6. Ожидание: Перенаправление в личный кабинет /cabinet
  7. Ожидание: Отображение уведомления о необходимости подтвердить 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}

Шаги

  1. Открыть /auth/login
  2. Ввести email
  3. Ввести пароль
  4. Нажать "Войти"
  5. Ожидание: Перенаправление в /cabinet
  6. Ожидание: В хедере отображается имя пользователя / аватар

Негативные сценарии

  • Неверный пароль → ошибка "Неверный 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

Шаги

  1. Войти в аккаунт с неподтверждённым email
  2. Увидеть баннер/уведомление "Подтвердите email"
  3. Проверить почту (для тестов — через API или mock)
  4. Ввести 6-значный код
  5. Нажать "Подтвердить"
  6. Ожидание: Баннер исчезает
  7. Ожидание: Доступна кнопка "Создать объявление"

Негативные сценарии

  • Неверный код → ошибка
  • Истёкший код (15 мин) → запросить повторно
  • Нажать "Отправить повторно" → новый код (пауза 60 сек между запросами)

UF-04: Сброс пароля

Роль: Гость URL: /auth/forgot-password/auth/reset-passwordПриоритет: High API: POST /auth/forgot-password{email}, POST /auth/reset-password{token, password}

Шаги

  1. На странице логина нажать "Забыли пароль?"
  2. Ввести email
  3. Нажать "Отправить ссылку"
  4. Перейти по ссылке из email → открывается /auth/reset-password?token=...
  5. Ввести новый пароль + подтверждение
  6. Нажать "Сбросить пароль"
  7. Ожидание: Перенаправление на логин с сообщением об успехе
  8. Войти с новым паролем

Негативные сценарии

  • Несуществующий 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

Шаги

  1. Открыть страницу каталога /catalog
  2. Увидеть список объявлений (карточки товаров)
  3. Прокрутить вниз → подгрузка следующей страницы (cursor-based pagination)
  4. Проверить карточку товара содержит:
    • Изображение (thumbnail)
    • Название
    • Цена
    • Город/район
    • Марка/модель авто (если указана)
  5. Нажать на карточку → переход на /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-запросы (см. ниже)

Шаги

  1. Открыть главную страницу
  2. В фильтрах найти блок "Автомобиль"
  3. Выбрать марку (Make) из выпадающего списка → загружаются модели
  4. Выбрать модель (Model) → загружаются поколения
  5. Выбрать поколение (Generation) → загружаются модификации
  6. (Опционально) Выбрать модификацию
  7. Нажать "Найти" / применить фильтр
  8. Ожидание: Список обновляется, показывает только подходящие запчасти
  9. Очистить фильтры → возврат к полному каталогу

Каскадная зависимость (YMMM)

Марка → Модель → Поколение → Модификация
  GET /store/cars/makes
    → GET /store/cars/makes/{id}/models
      → GET /store/cars/models/{id}/generations
        → GET /store/cars/generations/{id}/modifications

Playwright 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}

Шаги

  1. Перейти на страницу товара
  2. Проверить отображение:
    • Галерея изображений (слайдер/лайтбокс)
    • Название товара
    • Цена
    • Описание
    • OEM-номер (если есть)
    • Производитель
    • Категории (breadcrumbs)
    • Совместимость (марка/модель/поколение авто)
    • Местоположение (город, район, метро)
    • Признак "Комплект" (is_kit)
  3. Проверить блок продавца:
    • Имя
    • Аватар
    • Рейтинг и кол-во отзывов
    • Кнопка "Написать продавцу"
  4. Нажать "Написать продавцу":
    • Если гость → редирект на /auth/login
    • Если авторизован → создаётся conversation (POST /vendor/conversations) → редирект в /cabinet/messages/{id}
  5. Нажать на изображение → открытие лайтбокса
  6. Переключение между фото в лайтбоксе

Тест-кейсы (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

Шаги

  1. Перейти в личный кабинет /cabinet
  2. Нажать "Создать объявление"
  3. Заполнить форму (порядок полей — см. 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)
  4. Загрузить фото (мин. 1, макс. 10 — см. UF-10)
  5. Нажать "Сохранить черновик"
  6. Ожидание: Объявление создано со статусом draft
  7. Ожидание: Перенаправление на страницу объявления или список

Негативные сценарии

  • Без заголовка → валидация
  • Название > 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

Шаги

  1. Перейти в "Мои объявления" /cabinet/products
  2. Найти черновик
  3. Нажать "Редактировать"
  4. Изменить поля (название, цена, описание)
  5. Нажать "Сохранить"
  6. Ожидание: Данные обновлены
  7. Нажать "Опубликовать"
  8. Ожидание: Статус меняется на pending (на модерации)
  9. Ожидание: Отображается уведомление "Объявление отправлено на модерацию"

Статусы объявления

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}

Шаги

  1. Открыть редактирование объявления
  2. Загрузить новое изображение (кнопка загрузки, макс. 10МБ, макс. 10 фото на товар — DEV-303)
  3. Ожидание: Изображение появляется в галерее
  4. Перетащить фото для изменения порядка (drag-and-drop — DEV-305)
  5. Ожидание: Порядок сохранён (PUT /vendor/products/{id}/images/order с массивом image_ids)
  6. Удалить изображение → нажать крестик
  7. Ожидание: Изображение удалено; если удалено главное фото — главным автоматически становится следующее по порядку (DEV-303)

Edge-cases и UX-детали (DEV-303, DEV-305)

  • Достигнут лимит 10 фото → кнопка добавления скрывается, под галереей появляется хинт с текущим лимитом
  • Ошибка загрузки фото → рядом с превью отображаются иконка ошибки и кнопка повтора (retry)
  • Ни одно фото не помечено как основное → фолбэк: основным становится первое в порядке
  • URL-эндпоинты vendor-товаров вызываются через фабрику app/entities/product/api/endpoints.tsvendorProductEndpoints (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}

Шаги

  1. Открыть карточку товара
  2. Нажать "Написать продавцу"
  3. Ожидание: Создаётся conversation (POST /vendor/conversations)
  4. Ожидание: Перенаправление в чат /cabinet/messages/{id}
  5. Ввести сообщение "Здравствуйте, товар ещё в наличии?"
  6. Нажать отправить
  7. Ожидание: Сообщение появляется в чате
  8. Ожидание: У продавца — уведомление о новом сообщении

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)

Шаги

  1. Покупатель отправляет текстовое сообщение
  2. Продавец видит сообщение в реальном времени (Centrifugo WebSocket)
  3. Продавец отвечает
  4. Покупатель видит ответ в реальном времени
  5. Проверить индикатор "печатает..." (typing indicator через POST /conversations/{id}/typing)
  6. Отправить изображение через чат (POST /conversations/{id}/messages/image)
  7. Ожидание: Изображение загружается и отображается как thumbnail
  8. Нажать на изображение → открывается лайтбокс

Для тестирования 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=

Шаги

  1. Перейти в список сообщений /cabinet/messages
  2. Ввести запрос в поле глобального поиска (минимум 2 символа)
  3. Ожидание: Появляются результаты (сгруппированные по беседам)
  4. Нажать на результат → переход в беседу с подсветкой найденного сообщения
  5. Проверить highlight сообщения (оранжевая рамка)
  6. Поиск внутри конкретной беседы (ChatSearchBar)
  7. Навигация между найденными сообщениями (стрелки вверх/вниз)

UF-14: Добавление в избранное

Роль: Авторизованный пользователь URL: /product/{id}/cabinet/favoritesПриоритет: Medium API: POST /vendor/favorites{product_id}, DELETE /vendor/favorites/{product_id}, GET /vendor/favorites

Шаги

  1. Открыть карточку товара
  2. Нажать кнопку "В избранное" (сердечко/звезда)
  3. Ожидание: Кнопка меняет состояние (заполненная иконка)
  4. Перейти в /cabinet/favorites
  5. Ожидание: Товар отображается в списке
  6. Нажать "Удалить из избранного"
  7. Ожидание: Товар исчезает из списка

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

Шаги

  1. Перейти в /cabinet/settings/profile
  2. Изменить:
    • Имя (display_name)
    • Аватар (загрузить файл, макс. 5МБ)
    • Город → район → метро (geo cascade)
    • Тип аккаунта (личный / бизнес)
  3. Нажать "Сохранить"
  4. Ожидание: Данные обновлены
  5. Ожидание: Аватар обновлён в хедере

Примечание: Управление телефонами реализовано на frontend (PhoneManager). Телефон сохраняется как поле профиля через PUT /vendor/me.


UF-16: Управление сессиями

Роль: Авторизованный пользователь URL: /cabinet/settings/securityПриоритет: Low API: GET /vendor/sessions, DELETE /vendor/sessions/{id}, POST /auth/logout-all

Шаги

  1. Перейти в настройки безопасности /cabinet/settings/security
  2. Увидеть список активных сессий (IP, устройство, дата)
  3. Текущая сессия помечена
  4. Нажать "Завершить" рядом с другой сессией
  5. Ожидание: Сессия удалена из списка
  6. Нажать "Завершить все сессии" (POST /auth/logout-all)
  7. Ожидание: Все сессии кроме текущей удалены

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

Шаги

  1. Войти как администратор
  2. Перейти в /admin/products
  3. Увидеть список объявлений на модерации (status = pending)
  4. Нажать на объявление → просмотр деталей (GET /admin/products/{id})
  5. Проверить: фото, название, цена, описание, совместимость
  6. Сценарий A: Одобрить
    • Нажать "Одобрить"
    • Ожидание: Статус → active
    • Ожидание: Продавец получает уведомление
  7. Сценарий 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/*

Шаги

  1. Перейти в раздел справочников
  2. Марки:
    • Список всех марок
    • Добавить марку (имя, slug, is_popular)
    • Редактировать марку
    • Удалить марку (409 если есть дочерние модели или привязанные товары)
  3. Модели:
    • Выбрать марку → список моделей
    • Добавить модель
    • Редактировать модель
    • Удалить модель (409 если есть дочерние поколения или товары)
  4. Поколения:
    • Выбрать модель → список поколений
    • Добавить поколение (год от, руль)
    • Удалить поколение (409 если есть дочерние модификации или товары)
  5. Модификации:
    • Выбрать поколение → список модификаций
    • Добавить модификацию (тип топлива, привод, КПП)
  6. Каскадная защита (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}

Шаги

  1. Перейти в /admin/users
  2. Увидеть таблицу пользователей с фильтрами
  3. Фильтровать по типу аккаунта (personal/business)
  4. Нажать на пользователя → детали (контакты, аккаунт, статистика)
  5. Заблокировать пользователя → подтвердить → кнопка меняется на «Разблокировать»
  6. Разблокировать обратно → восстановить исходное состояние

Тест-кейсы (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

Шаги

  1. Войти как админ → автоматически /admin
  2. Проверить 4 карточки статистики:
    • Всего пользователей (+ новых сегодня)
    • Всего объявлений (+ новых сегодня)
    • На модерации (pending)
    • Черновики (draft)
  3. Нажать "На модерации" → переход к /admin/products
  4. Проверить блок "Пользователи" (по типу аккаунта)
  5. Проверить блок "Объявления" (по статусу)

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= — результаты

Шаги

  1. Кликнуть на поле поиска в хедере
  2. Начать вводить запрос (минимум 2 символа)
  3. Ожидание: Появляется выпадающий список саджестов (ProductSuggestion)
  4. Каждый саджест содержит:
    • Название товара
    • Цена
    • Thumbnail изображение
  5. Нажать на саджест → переход на /product/{id}
  6. Или: нажать Enter / "Найти все" → переход на /catalog?q={запрос}
  7. Ожидание: Каталог отфильтрован по запросу, доступна сортировка по релевантности

Негативные сценарии

  • Пустой запрос → саджесты не показываются
  • Менее 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

Шаги

  1. Нажать на индикатор города в хедере
  2. Ожидание: Открывается модальное окно выбора города
  3. Увидеть список регионов
  4. Выбрать регион → загружаются города
  5. Выбрать город
  6. Ожидание: Модалка закрывается
  7. Ожидание: Индикатор в хедере обновлён
  8. Ожидание: Город сохранён в cookie (персистенция между сессиями)
  9. Перейти в каталог → товары отфильтрованы по выбранному городу

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)

Шаги

  1. Перейти в /cabinet/settings/security
  2. Заполнить:
    • Текущий пароль
    • Новый пароль
    • Подтверждение нового пароля
  3. Нажать "Сменить пароль"
  4. Ожидание: Успешное уведомление
  5. Выйти и войти с новым паролем

Негативные сценарии

  • Неверный текущий пароль → ошибка
  • Новый пароль не соответствует требованиям → валидация
  • Пароли не совпадают → ошибка на клиенте

UF-24: Бизнес-профиль

Роль: Авторизованный пользователь (бизнес-аккаунт) URL: /cabinet/settings/businessПриоритет: Low API: PUT /vendor/me (поля business profile)

Шаги

  1. Перейти в /cabinet/settings/profile
  2. Переключить тип аккаунта на "Бизнес"
  3. Перейти на вкладку "Бизнес-профиль" (/cabinet/settings/business)
  4. Заполнить:
    • Название компании
    • ИНН
    • Адрес
    • Сайт
    • Часы работы
  5. Нажать "Сохранить"
  6. Ожидание: Данные обновлены
  7. Открыть публичный профиль продавца → отображается бизнес-информация

UF-25: Публичная страница продавца

Роль: Гость / Авторизованный URL: /seller/{id}Приоритет: Medium API: GET /store/sellers/{id}, GET /store/sellers/{id}/products

Шаги

  1. Перейти на /seller/{id} (через ссылку из карточки товара)
  2. Проверить отображение:
    • Аватар продавца
    • Имя / название компании
    • Рейтинг и количество отзывов
    • Город / район
    • Дата регистрации
    • Количество объявлений
  3. Проверить список товаров продавца (сетка карточек)
  4. Пагинация (cursor-based) при прокрутке
  5. Нажать на карточку товара → переход на /product/{id}
  6. Нажать "Написать продавцу":
    • Если гость → редирект на /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);

Тестовые данные

Пользователи

РольEmailПарольПримечания
Покупательbuyer@test.partizap.ruTest123!passemail verified
Продавецseller@test.partizap.ruTest123!passemail verified, есть объявления
Админadmin@test.partizap.ruAdmin123!passis_admin=true
Новыйnewuser@test.partizap.ruNew123!passemail НЕ 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 AuthhttpCredentials Playwright не обходит эту форму для browser navigation.

Решение:

  • gate.setup.ts навигирует на dev-сервер, заполняет форму, сабмитит → сохраняет cookies в GATE_STATE
  • Все проекты наследуют storageState: GATE_STATE — форма больше не показывается
  • httpCredentials в конфиге всё ещё нужен для request fixture (прямые API-вызовы в smoke-тестах)

Обработка 429 Rate Limit

API rate limiter считает попытки по IP за временное окно. В E2E-тестах многократные login/register вызывают 429.

Паттерны обработки:

  • Негативные тесты: .or() — принимаем либо ожидаемую ошибку, либо rate limit:
    typescript
    await expect(
      page.getByText(/неверный email или пароль/i)
        .or(page.getByText(/слишком много попыток/i)),
    ).toBeVisible()
  • Позитивные тесты: waitFor + test.skip — если rate limit, пропускаем тест:
    typescript
    const 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): RateLimitErrorsetup.skip() — пропускает зависимые тесты
  • CSRF/валидация ошибки: frontend отображает CSRF-ошибки как "Ошибка валидации". Тесты добавляют этот текст в .or() паттерны:
    typescript
    page.getByText(/неверный email или пароль/i)
      .or(page.getByText(/слишком много попыток/i))
      .or(page.getByText(/ошибка валидации/i))

PHP backend устанавливает CSRF_TOKEN cookie через Set-Cookie заголовок на API-запросы. В Playwright есть два API-контекста:

  • request fixture — отдельный HTTP-клиент со своим cookie jar. НЕ делится cookies с browser context
  • page.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 mode

CI/CD переменные

VariableНазначениеИспользуется в
E2E_BASE_URLURL стенда (default: https://dev.partizap.ru)playwright.config.ts
E2E_HTTP_USERЛогин формы dev gategate.setup.ts, httpCredentials
E2E_HTTP_PASSWORDПароль формы dev gategate.setup.ts, httpCredentials
E2E_USER_EMAILEmail тестового покупателяauth.setup.ts, functional tests
E2E_USER_PASSWORDПароль тестового покупателяauth.setup.ts, functional tests
E2E_SELLER_EMAILEmail тестового продавцаauth.setup.ts
E2E_SELLER_PASSWORDПароль тестового продавцаauth.setup.ts
E2E_ADMIN_EMAILEmail тестового админаauth.setup.ts
E2E_ADMIN_PASSWORDПароль тестового админаauth.setup.ts

Для локального запуска — файл .env.e2e (загружается в playwright.config.ts). В CI — GitLab CI/CD Variables (masked, not protected).


Порядок выполнения для записи тестов

Фаза 1: Базовые flows (без авторизации)

  1. UF-05: Каталог
  2. UF-07: Карточка товара
  3. UF-06: Поиск/фильтры

Фаза 2: Авторизация

  1. UF-01: Регистрация
  2. UF-02: Логин
  3. UF-03: Верификация email
  4. UF-04: Сброс пароля

Фаза 3: Продавец

  1. UF-08: Создание объявления
  2. UF-10: Изображения
  3. UF-09: Редактирование и публикация
  4. UF-15: Профиль
  5. UF-14: Избранное

Фаза 4: Чат

  1. UF-11: Начало чата
  2. UF-12: Обмен сообщениями
  3. UF-13: Поиск по сообщениям

Фаза 5: Админка

  1. UF-20: Дашборд
  2. UF-17: Модерация
  3. UF-18: Справочники
  4. UF-19: Пользователи
  5. UF-16: Сессии

Фаза 6: Дополнительные flows

  1. UF-21: Текстовый поиск и автокомплит
  2. UF-22: Выбор города
  3. UF-23: Смена пароля
  4. UF-24: Бизнес-профиль
  5. UF-25: Публичная страница продавца

Как записывать тесты

Через Gasoline MCP (рекомендуется)

  1. Запустить приложение локально (frontend dev server + backend)
  2. Открыть нужную страницу в Chrome с Gasoline extension
  3. Прокликать user flow вручную
  4. Gasoline записывает все действия (клики, ввод, навигацию)
  5. Сгенерировать Playwright-тест:
    gasoline generate --what=test --test_name="UF-01 Registration"
  6. Доработать сгенерированный тест (добавить 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.json

Backend API — полный реестр эндпоинтов (79 шт.)

Для сверки с тестами. Сгруппировано по группам маршрутов.

Auth (9)

MethodEndpointRate Limit
POST/auth/login5/15мин на email
POST/auth/register3/час на IP
POST/auth/forgot-password3/час на email
POST/auth/reset-password
POST/auth/logoutauth
POST/auth/logout-allauth
GET/auth/meauth
POST/auth/verify-email5/15мин на user, auth
POST/auth/resend-verificationauth

Store — публичные (12)

MethodEndpoint
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)

MethodEndpointОписание
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)

MethodEndpointОписание
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)

MethodEndpointОписание
GET/healthHealth check
POST/centrifugo/connectWS proxy
POST/centrifugo/disconnectWS proxy