Files
wiki/tasks/orchestrator/proposal_v1/09_ui_testing.md
2026-06-02 21:00:01 +03:00

19 KiB
Raw Permalink Blame History

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: в 20252026 Playwright стал мейнстримом для UI-тестирования (быстрее Cypress, кросс-браузерный из коробки, отличный visual regression, удобный для агентов через MCP). У него есть официальный MCP-сервер, который агенту даёт прямой контроль над браузером.


Test Plan: что именно тестируется

04-test-plan.yaml для UI-овой задачи содержит TC всех уровней. Пример:

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-тестами

Запуск регресса

# 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 для критичных экранов.

Минимальный шаблон теста:

// 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-атрибут на <html>.
  • prefers-reduced-motion уважается.

Cross-browser

Playwright поддерживает Chromium, Firefox, WebKit в одном API. Конфигурация:

// 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 укладываются в 510 минут.


Performance тесты

Lighthouse CI на ключевых страницах. Конфигурация:

{
  "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 или фича обрабатывает пользовательский ввод.

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-тестов

# .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-<id> с историей запусков.
    • В 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-тест на <App />, а не 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.