Skip to content

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

РольEmailПарольEnv var
Покупательbuyer@test.partizap.ruTest123!passE2E_USER_EMAIL / E2E_USER_PASSWORD
Продавецseller@test.partizap.ruTest123!passE2E_SELLER_EMAIL / E2E_SELLER_PASSWORD
Админadmin@test.partizap.ruAdmin123!passE2E_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

Каждая задача:

  1. Создаёт бранч от develop
  2. Делает работу + коммиты
  3. Мержится в 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-testid

Step 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.vueEmail inputlogin-email
login.vuePassword inputlogin-password
login.vueSubmit buttonlogin-submit
login.vueСсылка "Забыли пароль?"forgot-password-link
login.vueСсылка "Регистрация"register-link
register.vueФорма <UForm>register-form
register.vueDisplay name inputregister-name
register.vueEmail inputregister-email
register.vuePassword inputregister-password
register.vuePassword confirm inputregister-password-confirm
register.vueSubmit buttonregister-submit
SearchBarSearch inputsearch-input
SearchBarКаждый саджестsearch-suggestion
SearchBarКнопка очисткиsearch-clear
CitySelectorTrigger buttoncity-selector
CitySelectorModalcity-modal
CitySelectorApply buttoncity-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.vueproduct-card
  • Modify: app/pages/catalog/index.vuecatalog-sort, catalog-load-more, catalog-reset-filters, price-min, price-max
  • Modify: app/pages/product/[id].vueproduct-title, product-price, contact-seller, seller-card, product-description
  • Modify: app/features/ymm-select/ui/YmmSelect.vuemake-select, model-select, generation-select, modification-select
  • Modify: app/features/favorites/ui/FavoriteButton.vuefavorite-toggle
  • Modify: app/pages/seller/[id].vueseller-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.vueproduct-title, product-price, product-description, save-draft, publish-btn
  • Modify: app/pages/cabinet/messages/[id].vuemessage-input, send-message, messages-container
  • Modify: app/entities/conversation/ui/ConversationCard.vueconversation-card
  • Modify: app/pages/admin/products/index.vueproduct-row, filter-pending
  • Modify: app/pages/admin/index.vuestat-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-auth

Step 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 lint

Step 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-search

Step 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 lint

Step 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-product

Step 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 lint

Step 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-chat

Step 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 lint

Step 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