Appearance
DEV-202: Расширение E2E тестов — от smoke к функциональным
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Покрыть E2E-тестами все critical/high user flows (UF-01–UF-12, UF-17, UF-21), тесты проходят в CI на develop.
Architecture: Добавляем data-testid атрибуты в ключевые интерактивные компоненты, затем пишем функциональные Playwright-тесты по user flows из e2e-user-flows.md. Тесты используют API-логин через storageState (уже есть), три роли: guest, user/seller, admin. Структура тестов — по доменным группам (auth/, catalog/, seller/, chat/, admin/).
Tech Stack: Playwright, Nuxt 4.3, Vue 3.5, Nuxt UI v3
Тестовые данные (из e2e-user-flows.md):
| Роль | Пароль | Env var | |
|---|---|---|---|
| Покупатель | buyer@test.partizap.ru | Test123!pass | E2E_USER_EMAIL / E2E_USER_PASSWORD |
| Продавец | seller@test.partizap.ru | Test123!pass | E2E_SELLER_EMAIL / E2E_SELLER_PASSWORD |
| Админ | admin@test.partizap.ru | Admin123!pass | E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD |
Порядок выполнения
DEV-203 → DEV-204 → DEV-205 → DEV-206 → DEV-207
│ │ │ │ │
│ │ │ │ └─ feat/DEV-207-e2e-moderation-chat
│ │ │ └─ feat/DEV-206-e2e-create-product
│ │ └─ feat/DEV-205-e2e-catalog-search
│ └─ feat/DEV-204-e2e-auth
└─ feat/DEV-203-data-testidКаждая задача:
- Создаёт бранч от
develop - Делает работу + коммиты
- Мержится в
developперед началом следующей задачи
DEV-203: [UI] Добавить data-testid атрибуты + инфраструктура (1ч)
Branch: feat/DEV-203-data-testid
DoD: Все компоненты из e2e-user-flows.md имеют data-testid. Playwright конфиг и хелперы готовы для функциональных тестов.
Step 1: Создать бранч
bash
git checkout develop && git pull
git checkout -b feat/DEV-203-data-testidStep 2: Инфраструктура — обновить Playwright конфиг и хелперы
Files:
- Modify:
playwright.config.ts - Modify:
tests/e2e/auth.setup.ts - Modify:
tests/e2e/helpers/auth.helper.ts - Modify:
tests/e2e/helpers/auth.state.ts - Create:
tests/e2e/helpers/api.helper.ts - Modify:
.env.e2e.example
.env.e2e.example — добавить seller credentials:
bash
# Seller user credentials (has products, verified email)
E2E_SELLER_EMAIL=seller@test.partizap.ru
E2E_SELLER_PASSWORD=tests/e2e/helpers/auth.state.ts — добавить SELLER_STATE:
typescript
export const USER_STATE = 'tests/e2e/.auth/user.json'
export const SELLER_STATE = 'tests/e2e/.auth/seller.json'
export const ADMIN_STATE = 'tests/e2e/.auth/admin.json'tests/e2e/helpers/auth.helper.ts — добавить getSellerUser:
typescript
export function getSellerUser(): TestUser {
return {
email: process.env.E2E_SELLER_EMAIL || 'seller@test.partizap.ru',
password: process.env.E2E_SELLER_PASSWORD || 'Test123!pass',
}
}tests/e2e/auth.setup.ts — добавить seller setup:
typescript
import { SELLER_STATE } from './helpers/auth.state'
import { getSellerUser } from './helpers/auth.helper'
setup('authenticate as seller', async ({ request, page }) => {
setup.skip(
!process.env.E2E_SELLER_EMAIL || !process.env.E2E_SELLER_PASSWORD,
'E2E_SELLER_EMAIL / E2E_SELLER_PASSWORD not set',
)
await loginViaApi(request, page, getSellerUser())
await page.context().storageState({ path: SELLER_STATE })
})Create tests/e2e/helpers/api.helper.ts — хелпер для прямых API-вызовов:
typescript
import type { APIRequestContext } from '@playwright/test'
const BASE_URL = process.env.E2E_BASE_URL || 'https://dev.partizap.ru'
function getBasicAuthHeaders(): Record<string, string> {
const user = process.env.E2E_HTTP_USER
const pass = process.env.E2E_HTTP_PASSWORD || ''
if (!user) return {}
const encoded = Buffer.from(`${user}:${pass}`).toString('base64')
return { Authorization: `Basic ${encoded}` }
}
async function getCsrfToken(request: APIRequestContext): Promise<string> {
const cookies = await request.storageState()
return cookies.cookies.find((c) => c.name === 'CSRF_TOKEN')?.value ?? ''
}
export async function apiGet(request: APIRequestContext, path: string) {
return request.get(`${BASE_URL}/api${path}`, {
headers: getBasicAuthHeaders(),
})
}
export async function apiPost(
request: APIRequestContext,
path: string,
data?: Record<string, unknown>,
) {
const csrf = await getCsrfToken(request)
return request.post(`${BASE_URL}/api${path}`, {
data,
headers: { ...getBasicAuthHeaders(), 'X-CSRF-TOKEN': csrf },
})
}
export async function apiPut(
request: APIRequestContext,
path: string,
data?: Record<string, unknown>,
) {
const csrf = await getCsrfToken(request)
return request.put(`${BASE_URL}/api${path}`, {
data,
headers: { ...getBasicAuthHeaders(), 'X-CSRF-TOKEN': csrf },
})
}
export async function apiDelete(
request: APIRequestContext,
path: string,
) {
const csrf = await getCsrfToken(request)
return request.delete(`${BASE_URL}/api${path}`, {
headers: { ...getBasicAuthHeaders(), 'X-CSRF-TOKEN': csrf },
})
}playwright.config.ts — добавить проект functional:
typescript
projects: [
{
name: 'setup',
testMatch: /auth\.setup\.ts/,
},
{
name: 'smoke',
testDir: './tests/e2e/smoke',
use: { browserName: 'chromium' },
dependencies: ['setup'],
},
{
name: 'functional',
testDir: './tests/e2e',
testMatch: /\/(auth|catalog|seller|chat|admin)\/.*\.spec\.ts/,
use: { browserName: 'chromium' },
dependencies: ['setup'],
},
],Создать директории:
bash
mkdir -p tests/e2e/{auth,catalog,seller,chat,admin}Коммит:
bash
git add tests/e2e/ playwright.config.ts .env.e2e.example
git commit -m "test(DEV-203): add e2e infrastructure — seller auth, api helper, functional project"Step 3: data-testid — Header, Auth pages, Search, City selector
Files:
- Modify:
app/widgets/header/ui/AppHeader.vue - Modify:
app/pages/auth/login.vue - Modify:
app/pages/auth/register.vue - Modify:
app/features/search/ui/SearchBar.vue - Modify:
app/features/geo-select/ui/CitySelector.vue
Конвенция: {component}-{element} в kebab-case.
| Компонент | Элемент | data-testid |
|---|---|---|
| AppHeader | Кнопка/dropdown меню пользователя | user-menu |
| AppHeader | Кнопка входа (гость) | login-btn |
| AppHeader | Кнопка "Продать" | sell-btn |
| AppHeader | Кнопка выхода | logout-btn |
| login.vue | Форма <UForm> | login-form |
| login.vue | Email input | login-email |
| login.vue | Password input | login-password |
| login.vue | Submit button | login-submit |
| login.vue | Ссылка "Забыли пароль?" | forgot-password-link |
| login.vue | Ссылка "Регистрация" | register-link |
| register.vue | Форма <UForm> | register-form |
| register.vue | Display name input | register-name |
| register.vue | Email input | register-email |
| register.vue | Password input | register-password |
| register.vue | Password confirm input | register-password-confirm |
| register.vue | Submit button | register-submit |
| SearchBar | Search input | search-input |
| SearchBar | Каждый саджест | search-suggestion |
| SearchBar | Кнопка очистки | search-clear |
| CitySelector | Trigger button | city-selector |
| CitySelector | Modal | city-modal |
| CitySelector | Apply button | city-apply |
Коммит:
bash
git add app/widgets/header/ app/pages/auth/ app/features/search/ app/features/geo-select/
git commit -m "test(DEV-203): add data-testid to header, auth pages, search, city-selector"Step 4: data-testid — Catalog, Product, YMM, Favorites, Seller
Files:
- Modify:
app/entities/product/ui/ProductCard.vue—product-card - Modify:
app/pages/catalog/index.vue—catalog-sort,catalog-load-more,catalog-reset-filters,price-min,price-max - Modify:
app/pages/product/[id].vue—product-title,product-price,contact-seller,seller-card,product-description - Modify:
app/features/ymm-select/ui/YmmSelect.vue—make-select,model-select,generation-select,modification-select - Modify:
app/features/favorites/ui/FavoriteButton.vue—favorite-toggle - Modify:
app/pages/seller/[id].vue—seller-name
Коммит:
bash
git add app/entities/product/ app/pages/catalog/ app/pages/product/ app/features/ymm-select/ app/features/favorites/ app/pages/seller/
git commit -m "test(DEV-203): add data-testid to catalog, product, ymm, favorites, seller"Step 5: data-testid — Product form, Chat, Admin
Files:
- Modify:
app/features/product-form/ui/ProductFormPage.vue—product-title,product-price,product-description,save-draft,publish-btn - Modify:
app/pages/cabinet/messages/[id].vue—message-input,send-message,messages-container - Modify:
app/entities/conversation/ui/ConversationCard.vue—conversation-card - Modify:
app/pages/admin/products/index.vue—product-row,filter-pending - Modify:
app/pages/admin/index.vue—stat-card - Modify:
app/pages/admin/products/[id].vue(если есть) —approve-btn,reject-btn
Коммит:
bash
git add app/features/product-form/ app/pages/cabinet/messages/ app/entities/conversation/ app/pages/admin/
git commit -m "test(DEV-203): add data-testid to product-form, chat, admin"Step 6: Проверить lint + typecheck
bash
npm run lint && npm run typecheckПри ошибках — исправить и закоммитить:
bash
git commit -m "fix(DEV-203): fix lint/typecheck after data-testid"DEV-204: [Test] E2E авторизация через UI — UF-01, UF-02, UF-04 (1ч)
Branch: feat/DEV-204-e2e-authDepends on: DEV-203 merged into develop
Step 1: Создать бранч
bash
git checkout develop && git pull
git checkout -b feat/DEV-204-e2e-authStep 2: Написать tests/e2e/auth/login.spec.ts (UF-02)
typescript
import { test, expect } from '@playwright/test'
test.describe('UF-02: Авторизация через UI', () => {
test('форма логина отображает все поля', async ({ page }) => {
await page.goto('/auth/login')
await expect(page.locator('[data-testid="login-form"]')).toBeVisible()
await expect(page.locator('[data-testid="login-email"]')).toBeVisible()
await expect(page.locator('[data-testid="login-password"]')).toBeVisible()
await expect(page.locator('[data-testid="login-submit"]')).toBeVisible()
})
test('ссылка на регистрацию ведёт на /auth/register', async ({ page }) => {
await page.goto('/auth/login')
await page.locator('[data-testid="register-link"]').click()
await expect(page).toHaveURL(/\/auth\/register/)
})
test('ссылка "Забыли пароль?" ведёт на forgot-password', async ({ page }) => {
await page.goto('/auth/login')
await page.locator('[data-testid="forgot-password-link"]').click()
await expect(page).toHaveURL(/\/auth\/forgot-password/)
})
test('пустая форма показывает валидацию', async ({ page }) => {
await page.goto('/auth/login')
await page.locator('[data-testid="login-submit"]').click()
const form = page.locator('[data-testid="login-form"]')
const errors = form.locator('.text-error, [class*="error"]')
await expect(errors.first()).toBeVisible({ timeout: 3_000 })
})
test('неверные credentials показывают ошибку', async ({ page }) => {
await page.goto('/auth/login')
await page.locator('[data-testid="login-email"]').fill('wrong@nonexistent.email')
await page.locator('[data-testid="login-password"]').fill('WrongPassword123!')
await page.locator('[data-testid="login-submit"]').click()
await expect(page.getByText(/неверный|ошибка|invalid/i)).toBeVisible({ timeout: 10_000 })
})
test('успешный логин перенаправляет в кабинет', async ({ page }) => {
test.skip(
!process.env.E2E_USER_EMAIL || !process.env.E2E_USER_PASSWORD,
'E2E_USER_EMAIL / E2E_USER_PASSWORD not set',
)
await page.goto('/auth/login')
await page.locator('[data-testid="login-email"]').fill(process.env.E2E_USER_EMAIL!)
await page.locator('[data-testid="login-password"]').fill(process.env.E2E_USER_PASSWORD!)
await page.locator('[data-testid="login-submit"]').click()
await expect(page).toHaveURL(/\/cabinet/, { timeout: 15_000 })
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible()
})
})Step 3: Написать tests/e2e/auth/registration.spec.ts (UF-01)
typescript
import { test, expect } from '@playwright/test'
test.describe('UF-01: Регистрация', () => {
test('форма регистрации отображает все поля', async ({ page }) => {
await page.goto('/auth/register')
await expect(page.locator('[data-testid="register-form"]')).toBeVisible()
await expect(page.locator('[data-testid="register-name"]')).toBeVisible()
await expect(page.locator('[data-testid="register-email"]')).toBeVisible()
await expect(page.locator('[data-testid="register-password"]')).toBeVisible()
await expect(page.locator('[data-testid="register-password-confirm"]')).toBeVisible()
await expect(page.locator('[data-testid="register-submit"]')).toBeVisible()
})
test('пустая форма показывает валидацию', async ({ page }) => {
await page.goto('/auth/register')
await page.locator('[data-testid="register-submit"]').click()
const form = page.locator('[data-testid="register-form"]')
const errors = form.locator('.text-error, [class*="error"]')
await expect(errors.first()).toBeVisible({ timeout: 3_000 })
})
test('несовпадающие пароли показывают ошибку', async ({ page }) => {
await page.goto('/auth/register')
await page.locator('[data-testid="register-name"]').fill('Test User')
await page.locator('[data-testid="register-email"]').fill('test@example.com')
await page.locator('[data-testid="register-password"]').fill('StrongPass123!')
await page.locator('[data-testid="register-password-confirm"]').fill('DifferentPass456!')
await page.locator('[data-testid="register-submit"]').click()
await expect(page.getByText(/не совпадают|совпадение|match/i)).toBeVisible({ timeout: 5_000 })
})
test('существующий email показывает ошибку', async ({ page }) => {
test.skip(!process.env.E2E_USER_EMAIL, 'E2E_USER_EMAIL not set')
await page.goto('/auth/register')
await page.locator('[data-testid="register-name"]').fill('Duplicate User')
await page.locator('[data-testid="register-email"]').fill(process.env.E2E_USER_EMAIL!)
await page.locator('[data-testid="register-password"]').fill('StrongPass123!')
await page.locator('[data-testid="register-password-confirm"]').fill('StrongPass123!')
await page.locator('[data-testid="register-submit"]').click()
await expect(page.getByText(/уже зарегистрирован|already|существует/i)).toBeVisible({ timeout: 10_000 })
})
})Step 4: Написать tests/e2e/auth/password-reset.spec.ts (UF-04)
typescript
import { test, expect } from '@playwright/test'
test.describe('UF-04: Сброс пароля', () => {
test('страница forgot-password содержит форму с email', async ({ page }) => {
await page.goto('/auth/forgot-password')
await expect(page.locator('input[type="email"], input[name="email"]').first()).toBeVisible()
await expect(page.locator('button[type="submit"]').first()).toBeVisible()
})
test('отправка email не вызывает серверную ошибку', async ({ page }) => {
await page.goto('/auth/forgot-password')
await page.locator('input[type="email"], input[name="email"]').first().fill('test-reset@example.com')
await page.locator('button[type="submit"]').first().click()
// Backend всегда 200 (не раскрывает существование аккаунта)
await page.waitForTimeout(2_000)
const hasServerError = await page.getByText(/500|server error/i).isVisible().catch(() => false)
expect(hasServerError).toBeFalsy()
})
test('reset-password с невалидным токеном показывает ошибку', async ({ page }) => {
await page.goto('/auth/reset-password?token=INVALID_TOKEN_12345')
const errorAlert = page.locator('[class*="alert"]', { hasText: /недействительна|истекла/i })
const passwordForm = page.locator('form')
await expect(errorAlert.or(passwordForm).first()).toBeVisible({ timeout: 10_000 })
if (await errorAlert.isVisible()) {
await expect(page.locator('a[href="/auth/forgot-password"]')).toBeVisible()
}
})
})Step 5: Запустить и проверить
bash
npx playwright test --project=functional tests/e2e/auth/
npm run lintStep 6: Коммит
bash
git add tests/e2e/auth/
git commit -m "test(DEV-204): add functional e2e tests for auth (UF-01, UF-02, UF-04)"DEV-205: [Test] E2E каталог, фильтры, карточка — UF-05, UF-06, UF-07, UF-21 (2ч)
Branch: feat/DEV-205-e2e-catalog-searchDepends on: DEV-203 merged into develop
Step 1: Создать бранч
bash
git checkout develop && git pull
git checkout -b feat/DEV-205-e2e-catalog-searchStep 2: Написать tests/e2e/catalog/product-list.spec.ts (UF-05)
typescript
import { test, expect } from '@playwright/test'
test.describe('UF-05: Просмотр каталога (гость)', () => {
test('каталог отображает карточки товаров', async ({ page }) => {
await page.goto('/catalog')
const cards = page.locator('[data-testid="product-card"]')
await expect(cards.first()).toBeVisible({ timeout: 10_000 })
expect(await cards.count()).toBeGreaterThan(0)
})
test('карточка содержит название и цену', async ({ page }) => {
await page.goto('/catalog')
const firstCard = page.locator('[data-testid="product-card"]').first()
await expect(firstCard).toBeVisible({ timeout: 10_000 })
const text = await firstCard.textContent()
expect(text!.length).toBeGreaterThan(5)
})
test('клик на карточку → переход на /product/{id}', async ({ page }) => {
await page.goto('/catalog')
await page.locator('[data-testid="product-card"]').first().click()
await expect(page).toHaveURL(/\/product\/\d+/)
})
test('сортировка каталога работает', async ({ page }) => {
await page.goto('/catalog')
await page.locator('[data-testid="product-card"]').first().waitFor({ timeout: 10_000 })
const sortSelect = page.locator('[data-testid="catalog-sort"]')
if (await sortSelect.isVisible()) {
await sortSelect.click()
const option = page.getByRole('option').first()
if (await option.isVisible({ timeout: 3_000 }).catch(() => false)) {
await option.click()
}
}
await expect(page.locator('[data-testid="product-card"]').first()).toBeVisible()
})
test('cursor pagination — кнопка "Загрузить ещё"', async ({ page }) => {
await page.goto('/catalog')
await page.locator('[data-testid="product-card"]').first().waitFor({ timeout: 10_000 })
const loadMore = page.locator('[data-testid="catalog-load-more"]')
if (await loadMore.isVisible({ timeout: 3_000 }).catch(() => false)) {
const countBefore = await page.locator('[data-testid="product-card"]').count()
await loadMore.click()
await page.waitForTimeout(2_000)
const countAfter = await page.locator('[data-testid="product-card"]').count()
expect(countAfter).toBeGreaterThanOrEqual(countBefore)
}
})
})Step 3: Написать tests/e2e/catalog/product-detail.spec.ts (UF-07)
typescript
import { test, expect } from '@playwright/test'
test.describe('UF-07: Карточка товара', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/catalog')
await page.locator('[data-testid="product-card"]').first().click()
await expect(page).toHaveURL(/\/product\/\d+/)
})
test('отображает название и цену', async ({ page }) => {
await expect(page.locator('[data-testid="product-title"]')).toBeVisible()
await expect(page.locator('[data-testid="product-title"]')).not.toBeEmpty()
await expect(page.locator('[data-testid="product-price"]')).toBeVisible()
})
test('отображает блок продавца', async ({ page }) => {
await expect(page.locator('[data-testid="seller-card"]')).toBeVisible()
})
test('кнопка "Написать продавцу" видна', async ({ page }) => {
await expect(page.locator('[data-testid="contact-seller"]')).toBeVisible()
})
test('гость → клик "Написать" → редирект на логин', async ({ page }) => {
await page.locator('[data-testid="contact-seller"]').click()
await expect(page).toHaveURL(/\/auth\/login/)
})
test('галерея фото — лайтбокс при клике на изображение', async ({ page }) => {
const mainImage = page.locator('img').first()
if (await mainImage.isVisible()) {
await mainImage.click()
// Ждём появления лайтбокса (overlay/modal с увеличенным фото)
const lightbox = page.locator('[class*="lightbox"], [class*="modal"], [role="dialog"]').first()
if (await lightbox.isVisible({ timeout: 3_000 }).catch(() => false)) {
await expect(lightbox).toBeVisible()
}
}
})
})Step 4: Написать tests/e2e/catalog/search-filters.spec.ts (UF-06, UF-21)
typescript
import { test, expect } from '@playwright/test'
test.describe('UF-06: YMM-каскадный поиск', () => {
test('после выбора марки появляются модели', async ({ page }) => {
await page.goto('/catalog')
const makeSelect = page.locator('[data-testid="make-select"]')
await expect(makeSelect).toBeVisible({ timeout: 10_000 })
await makeSelect.click()
const firstOption = page.getByRole('option').first()
await expect(firstOption).toBeVisible({ timeout: 5_000 })
await firstOption.click()
const modelSelect = page.locator('[data-testid="model-select"]')
await expect(modelSelect).toBeVisible()
await expect(modelSelect).not.toBeDisabled({ timeout: 5_000 })
})
test('сброс фильтров работает', async ({ page }) => {
await page.goto('/catalog')
const makeSelect = page.locator('[data-testid="make-select"]')
await expect(makeSelect).toBeVisible({ timeout: 10_000 })
await makeSelect.click()
await page.getByRole('option').first().click()
const resetBtn = page.locator('[data-testid="catalog-reset-filters"]')
if (await resetBtn.isVisible({ timeout: 3_000 }).catch(() => false)) {
await resetBtn.click()
// Каталог показывает товары без фильтров
await expect(page.locator('[data-testid="product-card"]').first()).toBeVisible({ timeout: 10_000 })
}
})
})
test.describe('UF-21: Текстовый поиск и автокомплит', () => {
test('ввод запроса показывает саджесты', async ({ page }) => {
await page.goto('/')
const searchInput = page.locator('[data-testid="search-input"]')
await expect(searchInput).toBeVisible({ timeout: 10_000 })
await searchInput.fill('тор')
const suggestions = page.locator('[data-testid="search-suggestion"]')
await expect(suggestions.first()).toBeVisible({ timeout: 10_000 })
expect(await suggestions.count()).toBeGreaterThan(0)
})
test('клик на саджест → переход на страницу товара', async ({ page }) => {
await page.goto('/')
await page.locator('[data-testid="search-input"]').fill('тор')
await page.locator('[data-testid="search-suggestion"]').first().click()
await expect(page).toHaveURL(/\/product\/\d+/)
})
test('Enter → каталог с ?q=', async ({ page }) => {
await page.goto('/')
const searchInput = page.locator('[data-testid="search-input"]')
await searchInput.fill('колодки')
await searchInput.press('Enter')
await expect(page).toHaveURL(/\/catalog\?.*q=/)
})
test('менее 2 символов → саджесты не появляются', async ({ page }) => {
await page.goto('/')
await page.locator('[data-testid="search-input"]').fill('т')
await page.waitForTimeout(1_000)
const suggestions = page.locator('[data-testid="search-suggestion"]')
expect(await suggestions.count()).toBe(0)
})
})Step 5: Запустить и проверить
bash
npx playwright test --project=functional tests/e2e/catalog/
npm run lintStep 6: Коммит
bash
git add tests/e2e/catalog/
git commit -m "test(DEV-205): add functional e2e tests for catalog, product, YMM, search (UF-05, UF-06, UF-07, UF-21)"DEV-206: [Test] E2E создание и публикация объявления — UF-08, UF-09, UF-10 (2ч)
Branch: feat/DEV-206-e2e-create-productDepends on: DEV-203 merged into develop
Step 1: Создать бранч
bash
git checkout develop && git pull
git checkout -b feat/DEV-206-e2e-create-productStep 2: Написать tests/e2e/seller/create-product.spec.ts (UF-08)
typescript
import { test, expect } from '@playwright/test'
import { SELLER_STATE } from '../helpers/auth.state'
import { apiDelete } from '../helpers/api.helper'
test.describe('UF-08: Создание объявления', () => {
test.use({
storageState: process.env.E2E_SELLER_EMAIL ? SELLER_STATE : undefined,
})
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
testInfo.skip(
!process.env.E2E_SELLER_EMAIL || !process.env.E2E_SELLER_PASSWORD,
'E2E_SELLER_EMAIL / E2E_SELLER_PASSWORD not set',
)
})
test('форма создания отображает все ключевые поля', async ({ page }) => {
await page.goto('/cabinet/products/new')
await expect(page.locator('[data-testid="product-title"]')).toBeVisible({ timeout: 10_000 })
await expect(page.locator('[data-testid="product-price"]')).toBeVisible()
await expect(page.locator('[data-testid="product-description"]')).toBeVisible()
await expect(page.locator('[data-testid="save-draft"]')).toBeVisible()
})
test('пустая форма — submit показывает валидацию', async ({ page }) => {
await page.goto('/cabinet/products/new')
await page.locator('[data-testid="save-draft"]').click()
// Zod-валидация показывает ошибки обязательных полей
const errors = page.locator('.text-error, [class*="error"]')
await expect(errors.first()).toBeVisible({ timeout: 5_000 })
})
test('сохранение черновика с минимальными данными', async ({ page, request }) => {
await page.goto('/cabinet/products/new')
const title = `E2E тест ${Date.now()}`
await page.locator('[data-testid="product-title"]').fill(title)
await page.locator('[data-testid="product-price"]').fill('3500')
await page.locator('[data-testid="product-description"]').fill('Тестовое описание для E2E теста')
// TODO: заполнить обязательные поля (категория, гео) — зависит от формы
// Если форма требует их, тест может показать валидацию вместо сохранения
await page.locator('[data-testid="save-draft"]').click()
// Ожидаем: redirect или toast об успехе
const success = page.getByText(/черновик|сохранено|draft|успешно/i)
const validation = page.locator('.text-error, [class*="error"]')
await expect(success.or(validation).first()).toBeVisible({ timeout: 15_000 })
// Cleanup: если создан — удалить через API
// (cleanup опционален, при ошибке валидации товар не создаётся)
})
})Step 3: Написать tests/e2e/seller/edit-product.spec.ts (UF-09)
typescript
import { test, expect } from '@playwright/test'
import { SELLER_STATE } from '../helpers/auth.state'
test.describe('UF-09: Редактирование и публикация объявления', () => {
test.use({
storageState: process.env.E2E_SELLER_EMAIL ? SELLER_STATE : undefined,
})
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
testInfo.skip(
!process.env.E2E_SELLER_EMAIL || !process.env.E2E_SELLER_PASSWORD,
'E2E_SELLER_EMAIL / E2E_SELLER_PASSWORD not set',
)
})
test('список объявлений продавца загружается', async ({ page }) => {
await page.goto('/cabinet/products')
const products = page.locator('[data-testid="product-card"], [data-testid="product-row"]').first()
const emptyState = page.getByText(/нет объявлений|пусто|no products/i)
await expect(products.or(emptyState)).toBeVisible({ timeout: 10_000 })
})
test('переход к редактированию черновика', async ({ page }) => {
await page.goto('/cabinet/products')
const draftBadge = page.getByText(/черновик|draft/i).first()
if (await draftBadge.isVisible({ timeout: 5_000 }).catch(() => false)) {
// Переходим к редактированию
const card = draftBadge.locator('xpath=ancestor::a | ancestor::div[contains(@class, "card")]').first()
await card.click()
// Должна открыться форма с кнопками сохранения и публикации
await expect(
page.locator('[data-testid="save-draft"]').or(page.locator('[data-testid="publish-btn"]')),
).toBeVisible({ timeout: 10_000 })
}
})
test('кнопка публикации видна для черновика', async ({ page }) => {
await page.goto('/cabinet/products')
const draftBadge = page.getByText(/черновик|draft/i).first()
if (await draftBadge.isVisible({ timeout: 5_000 }).catch(() => false)) {
const card = draftBadge.locator('xpath=ancestor::a | ancestor::div[contains(@class, "card")]').first()
await card.click()
const publishBtn = page.locator('[data-testid="publish-btn"]')
await expect(publishBtn).toBeVisible({ timeout: 10_000 })
}
})
})Step 4: Запустить и проверить
bash
npx playwright test --project=functional tests/e2e/seller/
npm run lintStep 5: Коммит
bash
git add tests/e2e/seller/
git commit -m "test(DEV-206): add functional e2e tests for create/edit product (UF-08, UF-09, UF-10)"DEV-207: [Test] E2E модерация и чат — UF-17, UF-11, UF-12 (2ч)
Branch: feat/DEV-207-e2e-moderation-chatDepends on: DEV-203 merged into develop
Step 1: Создать бранч
bash
git checkout develop && git pull
git checkout -b feat/DEV-207-e2e-moderation-chatStep 2: Написать tests/e2e/admin/dashboard.spec.ts
typescript
import { test, expect } from '@playwright/test'
import { ADMIN_STATE } from '../helpers/auth.state'
test.describe('Админ — дашборд', () => {
test.use({
storageState: process.env.E2E_ADMIN_EMAIL ? ADMIN_STATE : undefined,
})
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
testInfo.skip(
!process.env.E2E_ADMIN_EMAIL || !process.env.E2E_ADMIN_PASSWORD,
'E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD not set',
)
})
test('дашборд отображает 4 карточки статистики', async ({ page }) => {
await page.goto('/admin')
const statCards = page.locator('[data-testid="stat-card"]')
await expect(statCards.first()).toBeVisible({ timeout: 10_000 })
expect(await statCards.count()).toBe(4)
})
})Step 3: Написать tests/e2e/admin/moderation.spec.ts (UF-17)
typescript
import { test, expect } from '@playwright/test'
import { ADMIN_STATE } from '../helpers/auth.state'
test.describe('UF-17: Модерация объявлений', () => {
test.use({
storageState: process.env.E2E_ADMIN_EMAIL ? ADMIN_STATE : undefined,
})
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
testInfo.skip(
!process.env.E2E_ADMIN_EMAIL || !process.env.E2E_ADMIN_PASSWORD,
'E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD not set',
)
})
test('список товаров загружается', async ({ page }) => {
await page.goto('/admin/products')
const productRows = page.locator('[data-testid="product-row"]').first()
const emptyState = page.getByText(/нет товаров|пусто/i)
await expect(productRows.or(emptyState)).toBeVisible({ timeout: 10_000 })
})
test('фильтр по статусу pending', async ({ page }) => {
await page.goto('/admin/products')
const pendingTab = page.locator('[data-testid="filter-pending"]')
if (await pendingTab.isVisible({ timeout: 5_000 }).catch(() => false)) {
await pendingTab.click()
await page.waitForLoadState('networkidle')
}
})
test('клик на товар → переход к деталям', async ({ page }) => {
await page.goto('/admin/products')
const firstRow = page.locator('[data-testid="product-row"]').first()
if (await firstRow.isVisible({ timeout: 5_000 }).catch(() => false)) {
await firstRow.click()
await expect(page).toHaveURL(/\/admin\/products\/\d+/, { timeout: 10_000 })
}
})
test('кнопки одобрить/отклонить видны на странице товара', async ({ page }) => {
await page.goto('/admin/products')
// Переходим к pending товару
const pendingTab = page.locator('[data-testid="filter-pending"]')
if (await pendingTab.isVisible({ timeout: 5_000 }).catch(() => false)) {
await pendingTab.click()
}
const firstRow = page.locator('[data-testid="product-row"]').first()
if (await firstRow.isVisible({ timeout: 5_000 }).catch(() => false)) {
await firstRow.click()
await expect(page).toHaveURL(/\/admin\/products\/\d+/)
const approveBtn = page.locator('[data-testid="approve-btn"]')
const rejectBtn = page.locator('[data-testid="reject-btn"]')
// Хотя бы одна из кнопок должна быть видна (зависит от текущего статуса)
await expect(approveBtn.or(rejectBtn)).toBeVisible({ timeout: 10_000 })
}
})
})Step 4: Написать tests/e2e/chat/start-conversation.spec.ts (UF-11)
typescript
import { test, expect } from '@playwright/test'
import { USER_STATE } from '../helpers/auth.state'
test.describe('UF-11: Начало чата с продавцом', () => {
test.use({
storageState: process.env.E2E_USER_EMAIL ? USER_STATE : undefined,
})
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
testInfo.skip(
!process.env.E2E_USER_EMAIL || !process.env.E2E_USER_PASSWORD,
'E2E_USER_EMAIL / E2E_USER_PASSWORD not set',
)
})
test('авторизованный → "Написать продавцу" → переход в чат', async ({ page }) => {
await page.goto('/catalog')
await page.locator('[data-testid="product-card"]').first().click()
await expect(page).toHaveURL(/\/product\/\d+/)
await page.locator('[data-testid="contact-seller"]').click()
await expect(page).toHaveURL(/\/cabinet\/messages\/\d+/, { timeout: 15_000 })
})
test('список бесед загружается', async ({ page }) => {
await page.goto('/cabinet/messages')
const conversations = page.locator('[data-testid="conversation-card"]').first()
const emptyState = page.getByText(/нет сообщений|пусто|начните/i)
await expect(conversations.or(emptyState)).toBeVisible({ timeout: 10_000 })
})
})Step 5: Написать tests/e2e/chat/messaging.spec.ts (UF-12)
typescript
import { test, expect } from '@playwright/test'
import { USER_STATE } from '../helpers/auth.state'
test.describe('UF-12: Обмен сообщениями', () => {
test.use({
storageState: process.env.E2E_USER_EMAIL ? USER_STATE : undefined,
})
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
testInfo.skip(
!process.env.E2E_USER_EMAIL || !process.env.E2E_USER_PASSWORD,
'E2E_USER_EMAIL / E2E_USER_PASSWORD not set',
)
})
test('открыть беседу → видно поле ввода и кнопку отправки', async ({ page }) => {
await page.goto('/cabinet/messages')
const first = page.locator('[data-testid="conversation-card"]').first()
if (await first.isVisible({ timeout: 5_000 }).catch(() => false)) {
await first.click()
await expect(page).toHaveURL(/\/cabinet\/messages\/\d+/)
await expect(page.locator('[data-testid="message-input"]')).toBeVisible({ timeout: 10_000 })
await expect(page.locator('[data-testid="send-message"]')).toBeVisible()
}
})
test('отправка текстового сообщения', async ({ page }) => {
await page.goto('/cabinet/messages')
const first = page.locator('[data-testid="conversation-card"]').first()
if (await first.isVisible({ timeout: 5_000 }).catch(() => false)) {
await first.click()
await expect(page).toHaveURL(/\/cabinet\/messages\/\d+/)
const input = page.locator('[data-testid="message-input"]')
await expect(input).toBeVisible({ timeout: 10_000 })
const msg = `E2E тест ${Date.now()}`
await input.fill(msg)
await page.locator('[data-testid="send-message"]').click()
await expect(page.getByText(msg)).toBeVisible({ timeout: 10_000 })
}
})
})Step 6: Запустить и проверить
bash
npx playwright test --project=functional tests/e2e/admin/ tests/e2e/chat/
npm run lintStep 7: Коммит
bash
git add tests/e2e/admin/ tests/e2e/chat/
git commit -m "test(DEV-207): add functional e2e tests for moderation and chat (UF-17, UF-11, UF-12)"Финальная проверка (после мержа всех задач)
bash
git checkout develop && git pull
npx playwright test --project=smoke # smoke не сломаны
npx playwright test --project=functional # все новые тесты проходят
npm run lint