# 09. Стратегия UI-тестирования **Назначение:** ваш отдельный пункт «нужно добиться полного тестирования от агентов включая тестирования UI» развернут в конкретный план: какие виды UI-тестов, на каком инструменте, как агент-Tester их запускает, как обновляются baseline'ы, как обрабатываются flaky. --- ## Простым языком UI-тестирование — самая тонкая часть автоматизации, потому что: - интерфейс зависит от шрифтов, рендеринга, времени, асинхронности; - результат «работает или нет» — частично визуальный (выглядит правильно), частично функциональный (нажал кнопку — событие произошло), частично доступный (слепой пользователь сможет пройти). Поэтому UI-тесты делятся на **четыре уровня**, каждый отвечает за свой аспект: 1. **Компонентные тесты** — проверяют, что отдельный кусочек UI ведёт себя правильно (вне браузера или в jsdom). 2. **E2E-тесты** — реальный браузер, реальные клики; проверяют сценарии пользователя из Acceptance Criteria. 3. **Visual regression** — сравнение скриншотов «было / стало». Защищает от случайных визуальных регрессий. 4. **A11y-тесты** — автопроверка доступности (контраст, ARIA, фокус, клавиатура). Плюс две сопровождающие проверки: **производительность** (Lighthouse, p95 latency) и **безопасность** (ZAP baseline для UI). Все эти тесты — обязательные ворота на QG-6. Без зелёного UI-теста задача с UI не уйдёт в деплой. --- ## Стек | Уровень | Инструмент | Где живут тесты | |---------|-----------|----------------| | Компонентные | Vitest / Jest + Testing Library | `tests/components/*.test.{ts,tsx}` | | E2E | **Playwright** (Chromium + Firefox + WebKit) | `tests/e2e/*.spec.ts` | | Visual regression | Playwright `toHaveScreenshot` или Loki / Chromatic | `tests/e2e/*` + `tests/visual/baseline/` | | A11y | `@axe-core/playwright` | внутри e2e тестов как доп. проверка | | Performance | Lighthouse CI | `tests/perf/lighthouse.config.json` | | Load | k6 / Locust | `tests/perf/load.js` | | Security | OWASP ZAP baseline | `tests/security/zap.conf` | > **Почему Playwright:** в 2025–2026 Playwright стал мейнстримом для UI-тестирования (быстрее Cypress, кросс-браузерный из коробки, отличный visual regression, удобный для агентов через MCP). У него есть официальный MCP-сервер, который агенту даёт прямой контроль над браузером. --- ## Test Plan: что именно тестируется `04-test-plan.yaml` для UI-овой задачи содержит TC всех уровней. Пример: ```yaml plane_id: PROJ-123 test_cases: # === Component-level === - id: TC-1 title: "NoiseZoneToggle renders with default state" type: unit priority: P1 automation: tool: vitest file: tests/components/NoiseZoneToggle.test.tsx coverage: [REQ-F-1] # === E2E === - id: TC-2 title: "User toggles noise zones layer on map" type: e2e priority: P0 automation: tool: playwright file: tests/e2e/noise-zones-toggle.spec.ts coverage: [REQ-F-1, AC-1] browsers: [chromium, firefox, webkit] - id: TC-3 title: "Mobile: noise zones legend collapses" type: e2e priority: P1 automation: tool: playwright file: tests/e2e/noise-zones-mobile.spec.ts viewport: { width: 375, height: 667 } coverage: [REQ-F-3] # === Visual regression === - id: TC-4 title: "Map with noise zones — visual baseline" type: visual priority: P0 automation: tool: playwright-visual file: tests/e2e/noise-zones.spec.ts snapshot: noise-zones-default threshold: 0.01 coverage: [REQ-NF-UI-1] # === A11y === - id: TC-5 title: "Noise zones panel — a11y AA compliance" type: a11y priority: P0 automation: tool: axe-core file: tests/e2e/noise-zones-a11y.spec.ts rules: [wcag2a, wcag2aa] coverage: [REQ-NF-A11Y-1] # === Performance === - id: TC-6 title: "Map load time with noise zones" type: performance priority: P1 automation: tool: lighthouse url: https://${PREVIEW_HOST}/map?layer=noise thresholds: performance: 90 accessibility: 95 LCP_ms: 2500 coverage: [REQ-NF-PERF-1] ``` --- ## Как агент-Tester работает с UI-тестами ### Запуск регресса ```bash # 1. Проверяет, что preview-окружение здорово curl -fsS $PREVIEW_URL/health || exit 1 # 2. Запускает все TC из test-plan python scripts/run-test-plan.py \ --plan docs/work-items/$PLANE_ID/04-test-plan.yaml \ --preview-url $PREVIEW_URL \ --output docs/work-items/$PLANE_ID/13-test-report/ # Скрипт парсит test-plan, для каждого TC вызывает соответствующий runner: # - vitest для unit # - playwright test для e2e и visual # - playwright + axe для a11y # - lighthouse-ci для perf ``` ### Через MCP (когда агент работает интерактивно) Playwright MCP даёт прямой контроль: ``` agent: playwright_navigate({ url: "https://pr-142.preview.example.com/map" }) agent: playwright_click({ selector: "[data-testid='noise-toggle']" }) agent: playwright_wait_for({ selector: "[data-testid='noise-layer']" }) agent: playwright_screenshot({ path: "screenshots/tc-2-after-toggle.png" }) ``` Полезно для интерактивной отладки или когда автотест есть, но требует расширенной диагностики. ### Обработка failing-теста 1. Tester-агент получает stack-trace и/или скриншот failing-теста. 2. Анализирует: это **баг кода** или **проблема теста**? - Если код не делает то, что в ТЗ — баг кода. Заводится Plane issue с шаблоном (`bug:found-by-qa`), привязка к Work Item, лейбл `back-to:dev`. - Если тест неверно описывает ожидание — это **проблема теста**, заводится отдельная задача `tech-debt:fix-flaky-test-X`, TC помечается `quarantined`, **не блокирует** релиз (с оговоркой: квота на quarantined ≤ 5% от тестов). 3. Если flaky (3 попытки, 2 раза падает, 1 раз проходит) — TC автоматически помечается `flaky`, задача в Plane, не блокирует релиз. --- ## Visual regression: подход **Стратегия:** использовать Playwright `toHaveScreenshot()` со снапшотами, хранящимися в `tests/visual/baseline/`. Снапшот — это PNG, версионируется в Git. **Threshold по умолчанию:** 0.01 (1% pixel difference). Можно ужесточать на критичных экранах. **Управление baseline'ами:** - При **первом** запуске теста (новый снапшот) — Playwright автоматически создаёт baseline и фейлит тест. Developer/Designer обновляет baseline через `playwright test --update-snapshots` локально и коммитит. - При **изменении дизайна** (намеренном) — Designer-агент обновляет baseline в своём этапе через Playwright MCP, кладёт новый PNG в `tests/visual/baseline/`. Diff приложен в комментарий PR. - **Любой diff в visual regression** → CI красный. Никакого «авто-обновления baseline'а в CI» — только через явное человеческое или агентское действие. **Что попадает в baseline:** - Скриншоты ключевых экранов в desktop (1280×800) и mobile (375×667). - На каждый ключевой компонент — отдельный визуальный тест в Playwright Component Testing. - Не каждый чих — только то, на что в ТЗ есть UI-требование. Иначе baseline'ы становятся неуправляемыми. **Что исключается:** - Динамические элементы (timestamps, рандомные данные, видео, GIF) — маскируются через `mask: [page.locator('.timestamp')]`. - Анимации — отключаются через `animations: 'disabled'`. --- ## A11y-тесты: подход **Инструмент:** `@axe-core/playwright`. Запускается на каждом затронутом экране. **Правила:** `wcag2a` + `wcag2aa` (по умолчанию). Опционально — `wcag2aaa` для критичных экранов. **Минимальный шаблон теста:** ```typescript // tests/e2e/noise-zones-a11y.spec.ts import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test('Noise zones panel: WCAG AA', async ({ page }) => { await page.goto('/map?layer=noise'); await page.locator('[data-testid="noise-toggle"]').click(); await page.waitForSelector('[data-testid="noise-layer"]'); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) .analyze(); expect(results.violations).toEqual([]); }); ``` **Что покрывается обязательно** (из чек-листа в `11-design/a11y.md`): - Контраст ≥ 4.5:1 для текста, ≥ 3:1 для UI-элементов. - Все интерактивные элементы доступны с клавиатуры (Tab/Shift-Tab, Enter, Space, Escape). - Focus visible (focus ring или аналогичный индикатор). - ARIA-роли для нестандартных компонентов. - Alt-тексты для изображений. - Lang-атрибут на ``. - `prefers-reduced-motion` уважается. --- ## Cross-browser Playwright поддерживает **Chromium, Firefox, WebKit** в одном API. Конфигурация: ```typescript // playwright.config.ts projects: [ { name: 'chromium', use: devices['Desktop Chrome'] }, { name: 'firefox', use: devices['Desktop Firefox'] }, { name: 'webkit', use: devices['Desktop Safari'] }, { name: 'mobile', use: devices['iPhone 13'] }, ] ``` **Принцип:** P0 e2e — на всех 4 проектах. P1 — на Chromium + один из (Firefox/WebKit) + mobile. P2/P3 — только Chromium. **В CI:** все запуски параллельны через matrix-strategy, на одном раннере 4-CPU укладываются в 5–10 минут. --- ## Performance тесты **Lighthouse CI** на ключевых страницах. Конфигурация: ```json { "ci": { "collect": { "url": ["http://localhost:3000/", "http://localhost:3000/map"], "numberOfRuns": 3 }, "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.9 }], "categories:accessibility": ["error", { "minScore": 0.95 }], "first-contentful-paint": ["error", { "maxNumericValue": 2500 }], "largest-contentful-paint": ["error", { "maxNumericValue": 4000 }], "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }] } } } } ``` **Load tests (k6 / Locust)** — для API-эндпоинтов с NFR по производительности. Запуск только при наличии REQ-NF-PERF в ТЗ. Не на каждый PR (медленно), а на nightly + перед мажорным релизом. --- ## Security baseline (UI) **OWASP ZAP** baseline scan — пассивный (без brute-force) скан preview-URL. Включается, если в ТЗ есть REQ-NF-SEC или фича обрабатывает пользовательский ввод. ```bash docker run --rm -v $(pwd)/tests/security:/zap/wrk \ ghcr.io/zaproxy/zaproxy:stable \ zap-baseline.py -t $PREVIEW_URL -g gen.conf -r zap-report.html ``` Алерты уровня High блокируют QG-6. Medium — issue в Plane, не блокируют. **Дополнительно:** Trivy на собранный образ (контейнер) — на каждом CI; npm audit / pip-audit / cargo audit — на каждом CI. --- ## Где живут тесты в репозитории ``` tests/ ├── components/ # vitest / jest, jsdom │ └── NoiseZoneToggle.test.tsx ├── e2e/ # playwright │ ├── noise-zones-toggle.spec.ts │ ├── noise-zones-mobile.spec.ts │ └── noise-zones-a11y.spec.ts ├── visual/ │ └── baseline/ # PNG снапшотов │ ├── chromium-desktop/ │ ├── firefox-desktop/ │ ├── webkit-desktop/ │ └── chromium-mobile/ ├── perf/ │ └── lighthouse.config.json │ └── load.js # k6 ├── security/ │ └── zap.conf ├── fixtures/ # сидируется в preview-окружение │ ├── users.json │ └── flights.csv ├── smoke/ # минимальный набор для smoke в test/prom │ └── api-health.spec.ts └── README.md # описание структуры тестов ``` --- ## CI: пайплайн UI-тестов ```yaml # .github/workflows/qg-test.yml name: QG-6 Test on: pull_request: types: [labeled] jobs: e2e: if: github.event.label.name == 'stage:test' runs-on: ubuntu-latest strategy: matrix: project: [chromium, firefox, webkit, mobile] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test --project=${{ matrix.project }} - if: failure() uses: actions/upload-artifact@v4 with: name: playwright-${{ matrix.project }} path: | test-results/ playwright-report/ visual: needs: e2e runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npx playwright test --grep @visual - run: ./scripts/visual-diff-summary.sh > docs/work-items/${PLANE_ID}/13-test-report/visual-diff.md a11y: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npx playwright test --grep @a11y perf: runs-on: ubuntu-latest steps: - uses: treosh/lighthouse-ci-action@v11 with: configPath: ./tests/perf/lighthouse.config.json security-baseline: runs-on: ubuntu-latest if: contains(github.event.pull_request.labels.*.name, 'has-ui') steps: - run: ./scripts/zap-baseline.sh $PREVIEW_URL generate-report: needs: [e2e, visual, a11y, perf] runs-on: ubuntu-latest if: always() steps: - uses: actions/checkout@v4 - run: ./scripts/generate-test-report.py > docs/work-items/${PLANE_ID}/13-test-report.md - uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "test(qa): test report for PROJ-123" ``` --- ## Flaky tests: процедура Flaky — тест, который при одном и том же коде иногда проходит, иногда падает. 1. Detection: CI runner ведёт счётчик. Если тест в течение 30 дней падал и проходил на одном и том же SHA — он flaky. 2. Tester-агент при обнаружении flaky: - Помечает TC в test-plan: `quarantined: true, reason: flaky-N-times-in-7-days`. - Заводит Plane issue `tech-debt:flaky-test-` с историей запусков. - В test-report указывает: «N TC quarantined, не блокирует релиз». 3. Quarantined тесты запускаются в CI, но падение не блокирует merge. Сводка в test-report. 4. **Лимит карантина:** ≤5% от общего числа тестов. При превышении — лейбл `escalation:test-quality` на проект, обязательное вмешательство Owner. --- ## Когда UI-тестов нет — что делать Если задача не затрагивает UI (`ui_affected: false` в ТЗ), Designer-этап автозакрывается, **никаких UI-тестов не пишется**. Tester ограничивается unit/integration/perf/security. --- ## Антипаттерны UI-тестирования - ❌ «Smoke test» для UI: только зайти на главную и проверить, что не упало. Это unit-тест на ``, а не e2e. - ❌ Тестировать через `data-testid`, расставленные **только для тестов**. Лучше — тестировать через ARIA-роли и видимый текст. - ❌ Sleep'ы в e2e (`await page.waitForTimeout(2000)`). Использовать `waitForSelector`/`waitForLoadState`/`waitForResponse`. - ❌ Хардкодить URL preview в тесте. Только через env-переменную `BASE_URL`. - ❌ Запускать UI-тесты против test/prom. Только preview. - ❌ Игнорировать визуальный diff «авось не страшно». Любой diff = либо обновить baseline (намеренно), либо вернуть в Dev. - ❌ Тестировать на одном браузере. Минимум Chromium + WebKit (последний — приближение к Safari/iOS). - ❌ Хранить скриншоты ≥1MB в Git. Использовать gzip / quality 80, либо вынести в LFS. - ❌ Скрипт `playwright test --update-snapshots` на CI без явного флага. Только локально или через явный workflow.