Skip to content
Back to skills

Test Review

ASecurity

Ревью только что написанных или изменённых автотестов на соответствие best practices TypeScript + Playwright (по официальной документации) и конвенциям вашего проекта. Используй по /test-review либо после написания/правки любого теста (UI E2E, API, UI+API, моки, visual, mobile) или Page Object/фикстуры/констант — до коммита. Работает в изолированном субагенте без истории сессии. Выдаёт приоритизированный список замечаний с severity, привязкой к строкам и готовыми фиксами.

  • 13 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added August 31, 2026
testingtypescriptgobashreactvuedockergitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 5, 2026

npx -y skills add akovalion/paranoid-qa --skill test-review --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Test Review?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Test Review
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/akovalion-test-review/badge)](https://www.skillsdirectory.com/skills/akovalion-test-review)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: test-review
description: Ревью только что написанных или изменённых автотестов на соответствие best practices TypeScript + Playwright (по официальной документации) и конвенциям вашего проекта. Используй по /test-review либо после написания/правки любого теста (UI E2E, API, UI+API, моки, visual, mobile) или Page Object/фикстуры/констант — до коммита. Работает в изолированном субагенте без истории сессии. Выдаёт приоритизированный список замечаний с severity, привязкой к строкам и готовыми фиксами.
context: fork
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash
---

# Ревью автотестов (TypeScript + Playwright)

Проверь только что написанный или изменённый тест-код на соответствие best practices и выдай приоритизированный список замечаний с фиксами. Источник правил — **официальная документация Playwright** ([best-practices](https://playwright.dev/docs/best-practices), [locators](https://playwright.dev/docs/locators), [test-assertions](https://playwright.dev/docs/test-assertions)) и [TypeScript](https://playwright.dev/docs/test-typescript) + конвенции конкретного проекта.

> **Источник правды по проекту** — его корневой `CLAUDE.md` (если есть) и стиль соседнего кода. Этот скилл — **фаза проверки**: он дополняет проектные правила, не заменяет их. Развёрнутые `❌ до → ✅ после` и ссылки на источники по каждому правилу — в [`references/rules-catalog.md`](references/rules-catalog.md).

---

Ты - независимый ревьюер. Истории сессии, где писали код, у тебя нет, и это намеренно: ревьюер, который видел, почему принято каждое решение, склонен с ними соглашаться. Оценивай код как он есть, без объяснений автора.

Scope от вызывающего: $ARGUMENTS

## Когда применять и режим работы

- **Только диагностика.** Прочитать код, прогнать статический анализ, выдать отчёт. Файлы не правишь: отчёт уходит вызывающей сессии, фиксы применяет она.
- **Первым делом прочитай корневой `CLAUDE.md` проекта** (если есть): в изолированном контексте он может не подгрузиться.
- **Scope — только новое/изменённое, не весь suite.** Пути из аргументов; без них - незакоммиченные изменения (`git status` + `git diff`).
- **Любой тип теста:** UI E2E, API, UI+API, visual regression, mobile, моки, а также Page Object, фикстуры, константы.
- Тесты прогоняешь только если об этом просят в аргументах. Весь suite и браузер - никогда.

## Доказательная дисциплина (без галлюцинаций)

- Каждое замечание — по **реально прочитанной строке** (`file:line`) или по **наблюдённому выводу** typecheck / lint / прогона. Не выдумывай нарушения «по аналогии» и не ссылайся на строки, которых не видел.
- Правило проверяется инструментом (tsc, ESLint, прогон) → **сначала запусти инструмент, потом репорти его вывод**, а не «вероятно есть».
- Не уверен, что это дефект, а не осознанное решение проекта → помечай **«под вопросом»**, не утверждай. Сверяйся с `CLAUDE.md` проекта и соседним кодом: часть «анти-паттернов» может быть намеренной (легаси-хелперы, нестандартная разметка, осознанные исключения из правил). Имена файлов/эндпоинтов/селекторов из памяти и прошлого контекста — фон, перепроверяй на живом коде.
- Решение, которое без объяснения автора выглядит нарушением, не угадывай и не оправдывай. Выноси его в «Под вопросом» с конкретным вопросом автору.
- В отчёт - только прогоны, которые сделал сам.
- Не нашёл нарушений в категории — так и пиши «чисто», не придумывай замечание ради объёма.

## Процесс

1. **Scope.** Определи файлы под ревью: `git status --short` + `git diff --name-only` (учитывай untracked), либо переданные пути. Для каждого spec найди связанные Page Object'ы, константы, фикстуры.
2. **Контекст.** `Read` изменённых файлов + связанных POM/констант/фикстур. `Read` соседнего spec в той же папке как эталон стиля. Сверь правила директории/сьюта (smoke / regress / api и т.п.) по `CLAUDE.md` проекта, если он есть.
3. **Статический анализ (обязательно — дёшево и доказательно):**
   - Typecheck: `tsc --noEmit` (или typecheck-скрипт проекта из `package.json`). Любая ошибка типов в новом коде = 🔴 Blocker.
   - ESLint: найди конфиг проекта и **прочитай, какие правила реально включены** (особенно из `eslint-plugin-playwright`) — не предполагай по памяти. Вывод линта — источник правды.
   - **Что реально ловит линт — проверка двухуровневая.** (1) Плагина eslint-plugin-playwright нет вообще → floating promise, ручные ассерты и `networkidle` невидимы; предложи подключить recommended. (2) Recommended подключён → `missing-playwright-await`, `prefer-web-first-assertions`, `no-networkidle` стоят error, но каждое покрывает меньше, чем обещает название (сверено по исходникам плагина):
     - `missing-playwright-await` проверяет только асинхронные матчеры, `expect.poll`, `test.step` и `waitFor*`. Действие без `await` (`page.click()`, `page.goto()`, `locator.fill()`) проходит молча, если у правила нет `includePageLocatorMethods: true` (в recommended его нет) и в проекте нет type-aware `@typescript-eslint/no-floating-promises`. Нет ни того, ни другого → пропущенный `await` у действий линт не видит; предложи одно из двух.
     - `prefer-web-first-assertions` срабатывает только с `toBe`/`toEqual`/`toBeTruthy`/`toBeFalsy`. `expect(await loc.getAttribute('href')).toContain(…)` или `toMatch(…)` проходят.
     - `expect(await loc.count()).toBe(n)` ловит `prefer-to-have-count`, а оно в recommended только **warn**.
     - `no-wait-for-timeout`, `no-force-option` и `expect-expect` там тоже только **warn** - без `--max-warnings 0` эти предупреждения не валят CI. Предложи поднять их до error.
     Что осталось вне линта - проверяй вручную (A/C/H).
   - При необходимости — проверка форматирования (Prettier), если настроена в проекте.
4. **Чеклист.** Пройди категории A–J ниже + K (правила вашего проекта). На каждое нарушение — severity + `file:line` + фикс. Глубже по правилу — [`references/rules-catalog.md`](references/rules-catalog.md).
5. **Верификация стабильности** (только если в аргументах просят проверить стабильность): прогон **только этого теста** в нативном параллелизме проекта (НЕ `--workers=1`):
   ```bash
   npx playwright test <file> --grep "<id>" --project="<projectName>" --retries=0 --repeat-each=5
   ```
   Pass-на-ретрае или плавающий результат = флак = 🟠 Major, чинить причину (гонки/гидратация/ожидания), а не прятать за retries.
6. **Отчёт** — в формате из раздела «Формат отчёта».

## Severity

| Метка | Значение | Типичные примеры |
|---|---|---|
| 🔴 **Blocker** | Тест сломан, недетерминирован или маскирует баг. Не мержить. | Ошибка typecheck; пропущенный `await` (floating promise); `waitForTimeout`/in-page `setTimeout`-пауза; pass только на ретрае; `{ force: true }` / `dispatchEvent` / прямой `setter` в обход реального UI; тест без ассертов; `test.only`; условный `expect`, который может не выполниться. |
| 🟠 **Major** | Хрупкость или флак при смене контента/окружения; нарушение ключевого правила проекта. | CSS/XPath-цепочки вместо role/label; мгновенный `count()`/`isVisible()`/`allTextContents()` как gate; точные цены/тексты/даты вместо regex; нарушение конвенции проекта (импорт базового `@playwright/test` там, где проект требует кастомную фикстуру; пропущена обязательная проектная проверка — напр. монитор сетевых ошибок); `waitForLoadState('networkidle')`; тест зависит от состояния другого. |
| 🟡 **Minor** | Стиль/читаемость/поддерживаемость; на стабильность не влияет. | Нет `test.step` по бизнес-шагам; инлайн-комментарии вместо самодокументирования; `.nth()` где годится `.filter()`; рабочий, но не приоритетный локатор; неиспользуемый импорт/константа. |
| ⚪ **Nit** | Косметика. | Именование, порядок импортов, форматирование (если не ловит Prettier). |

---

## Чеклист ревью

Каждый пункт — что искать; в скобках — severity нарушения. Развёрнутые примеры и пруфы — в каталоге.

### A. Детерминизм, ожидания, асинхронность
- [ ] Нет `page.waitForTimeout(ms)` и нет `setTimeout`/`sleep` внутри `page.evaluate` (грепни оба). Ждать состояние, не время. (🔴)
- [ ] Нет «висящих» промисов: каждый `expect`, `test.step`, действие (`click/fill/goto`), `waitFor*` — под `await`/`return`/`void`. Пропущенный `await` = молчаливый флак. (🔴)
- [ ] Сеть — паттерн **promise → действие → await**: `const p = page.waitForResponse(...); await click(); await p`. Объявление после действия = гонка. (🔴)
- [ ] Нет `waitForLoadState('networkidle')`. Навигация — `goto(url, { waitUntil: 'domcontentloaded' })`, без дублирующего `waitForLoadState` следом. (🟠)
- [ ] Производные/составные проверки (несколько связанных условий, замер коллекции сразу после появления) — в `await expect(async () => {...}).toPass({ timeout })`, а не цепочка `await`-ов. (🟠)
- [ ] Учтена SSR-гидратация: SSR-фреймворки (Nuxt/Next и др.) могут перемонтировать контент после гидратации → мгновенный `count()`/`allTextContents()` сразу после появления ловит окно пустоты. Замер через `toPass`. (🟠)

### B. Локаторы
- [ ] Приоритет: `getByRole({ name })` → `getByLabel` (поля формы) / `getByText` (статичный контент) → `getByPlaceholder` → `getByAltText` → `getByTitle` → `getByTestId` → CSS (крайний случай) → XPath (почти никогда). (🟠 при CSS/XPath без причины)
- [ ] Нет хрупких CSS-цепочек по структуре DOM (`div > div > span`, `.episode-actions-later`). Ломаются при ребрендинге. (🟠)
- [ ] Strict mode: локатор резолвится в один элемент; уточнение через `{ name }` / `.filter({ hasText })` / `.filter({ has })`, а не `.nth()`. `.nth()` — только с обоснованием. (🟡)
- [ ] Плавающие элементы (дропдауны, тосты, модалки, портальный контент, iframe) ищутся **глобально от `page`**, не от секции. (🟠)
- [ ] Длинный `getByText('целое предложение')` не используется как якорь — хрупко к правкам копирайта; брать стабильный фрагмент/role. (🟡)

### C. Ассерты
- [ ] Только **web-first** (авто-ретрай): `toBeVisible/toHaveText/toHaveCount/toHaveValue/toBeChecked/toHaveAttribute/toHaveURL`. Нет `expect(await loc.isVisible()).toBe(true)` и `expect(await loc.count()).toBe(n)` — не ретраятся. (🔴/🟠)
- [ ] Каждый тест что-то проверяет (нет теста, который только кликает без `expect`). (🔴)
- [ ] Блок независимых проверок одной секции — через `expect.soft`, чтобы собрать все падения разом. (🟡)
- [ ] Нет ассертов на точные цены/числа/даты/динамический контент — regex или диапазон. (🟠)
- [ ] Известный незакрытый баг — `test.fail()` (с единственным баг-ассертом), не `test.fixme()`; рабочее поведение — отдельным обычным тестом. (🟡)

### D. Изоляция и независимость
- [ ] Тесты независимы: состояние НЕ передаётся между тестами. `let x` на уровне `describe`, переинициализируемый в `beforeEach`, — распространённый валидный паттерн; нарушение — когда тест читает результат другого. Прогон в одиночку и в любом порядке должен проходить. (🔴 если ломает изоляцию)
- [ ] `describe.configure({ mode: 'serial' })` — только при реальной зависимости, не «на всякий случай». (🟠)
- [ ] Setup/teardown — в `beforeEach`/фикстурах, без копипасты; созданные сущности (API) удаляются. (🟠)
- [ ] Тест не зависит от внешних сайтов и third-party виджетов — тестируем только то, что контролируем; внешнее — мок/проверка факта запроса. (🟠)

### E. TypeScript и линт
- [ ] Typecheck зелёный для нового кода (`tsc --noEmit`, strict). (🔴)
- [ ] `any` в **POM / fixtures / utils** — нежелателен, типизируй (`Locator`/`Page`/`Route`/`APIResponse`). Сверь с конфигом проекта: `any` может быть осознанно разрешён в спеках (напр. для мок-данных) — тогда там не флагай. `@ts-ignore` — только с причиной/тикетом. (🟠 для POM/utils)
- [ ] Поля POM — `readonly Locator`; фикстуры типизированы (`base.extend<{...}>`); тело ответа API типизируй явно, если на него опираются ассерты. (🟡)
- [ ] **Пропущенный `await` у действия линт обычно НЕ ловит**: `missing-playwright-await` покрывает его только с `includePageLocatorMethods: true`, иначе нужен type-aware `no-floating-promises`; промис под `void` проходит даже его → перечитай глазами, см. A2. (🔴)
- [ ] Сверь модульную систему (ESM vs CJS) и стиль импортов (относительные vs алиасы) с фактическим кодом проекта — следуй существующему стилю, не навязывай свой. Неиспользуемые импорты/переменные/константы/методы POM убрать. (🟡)

### F. Сеть и моки
- [ ] Моки (`page.route`) — только для edge cases (5xx, пустой ответ, таймаут, офлайн). Позитивный happy-path — против реального API. (🟠)
- [ ] Проверка контракта, где это суть теста: `waitForResponse` (статус + тело) / `waitForRequest` + `postDataJSON()`. Для форм — инспекция payload на `[object Object]`, пустые/несериализованные поля, а не «кнопка активна». (🟠)
- [ ] Роуты ставятся **до** триггерящего действия; область — тест/фикстура, не глобально на suite. (🟠)
- [ ] Внешний хост, который может не отвечать (напр. внешний личный кабинет, платёжный шлюз): переход не проверяем «вглубь» — оракул это инициированный навигационный запрос, а не загрузка цели. Гасить запрос по ситуации: `route.abort()` годится, только если тест на этом заканчивается; если тесту дальше жить на странице (повторный `goto`, следующие шаги) — `abort()` навигации уводит Chromium в `chrome-error://` и ломает следующую навигацию, а `204` Chromium всё равно трактует как переход. Рабочий вариант — `route.fulfill` html-заглушкой (`200 text/html`) + повторный `goto`. (🟠)
- [ ] Свои `route` снимаются точечно — `page.unroute(matcher, handler)`. `unrouteAll()` сносит и роуты, поставленные фикстурами проекта (заглушки зависших third-party хостов и т.п.) — после него другие тесты/шаги флачат. (🟠)

### G. Структура, читаемость, гигиена
- [ ] Логические шаги обёрнуты в `test.step('Императив', …)` (видно в Allure/HTML/trace). `return` — снаружи коллбэка. Не дробить на каждое действие. (🟡)
- [ ] **Нет инлайн-комментариев** в тестах — самодокументирование (осмысленные имена, semantic-локаторы, шаги). Контекст — в описании/аннотации репортера (напр. `allure.description`), если проект их использует. (🟡)
- [ ] Параметризация однотипных кейсов через `for...of` **снаружи** `test.describe`, а не копии теста. (🟡)
- [ ] Нет `test.only`, закомментированных тестов, временных файлов/черновиков, отладочных `console.log`/`page.pause()`. (🔴 для `test.only`/`page.pause`, иначе 🟡)
- [ ] Имена тестов/шагов осмысленны; формат ID/тегов (`@allure.id:N`, ключ ТК и т.п.) — как у соседних тестов в файле. (⚪)

### H. Маскировка багов и флак
- [ ] Нет синтетических обходов реального UX: `{ force: true }`, `dispatchEvent`, прямой React/Vue-setter, ручной скролл вместо авто-actionability — если только это не оправдано контролируемым input'ом (напр. кастомные `display:none` инпуты — проверь в браузере). Фикс должен ловить регрессию, если фича сломается, а не прятать её. (🔴)
- [ ] `retries`/`mode: serial`/увеличенный timeout не используются как «лекарство» от флака. Карантин допустим только временно, со ссылкой на тикет. (🟠)
- [ ] `try/catch` не глушит падения действий/ассертов (auto-waiting встроен; `.catch()` прячет баг). (🟠)
- [ ] Пред-релизный тест (написан до выката фичи) **падает честно**, не спрятан за `skip`/флагом. (🟠)
- [ ] Упавший тест **продиагностирован до фикса**: баг продукта или плановое изменение? Доказательства, не догадка — *когда* сломалось (история прогонов; группа тестов, покрасневшая в одну дату, — релиз, а не дрейф контента), тот же элемент *сравнён между окружениями* (есть на стенде, пропал на проде → регрессия прода; нет на обоих → выкачено осознанно), *внутренняя асимметрия* (виден на мобильном, но не на десктопе; есть в DOM, но скрыт CSS; только одно плечо A/B). Вердикт «баг» → сообщить и оформить ассерт как `test.fail` с тикетом, а не подгонять его под текущий DOM. (🔴)
- [ ] Фикс не **ослабляет оракул** ради зелёного: `toBeVisible()` → `toBeAttached()` (скрытый CSS элемент начинает проходить), адресный ассерт заменён щедрым счётчиком, проверка удалена со ссылкой «покрыто в другом месте» без чтения того спека. Погасить сигнал — не значит починить. (🔴)

### I. Спецслучаи по типу теста
- [ ] **API:** проверяется статус И тело; идентификаторы запросов — свежий `randomUUID()` из встроенного `crypto` на каждый запрос (не тащи пакет `uuid`, если его нет в проекте); учтён rate limit; cleanup созданного. (🟠)
- [ ] **iframe:** `frameLocator`; контент ищется внутри фрейма. **Новый таб:** `context.waitForEvent('page')`. **Download:** `waitForEvent('download')` + проверка имени. **Upload:** `setInputFiles`. **Время:** `page.clock`. **Геолокация/права:** `grantPermissions`/`setGeolocation`. (🟠 при ручных обходах)
- [ ] **visual:** `toHaveScreenshot` с `animations:'disabled'` и `mask` на динамику; эталоны — на платформе CI (macOS-эталон против Linux-CI = гарантированный diff). Только если тест-кейс требует эталон. (🟠)

### J. Соответствие намерению (оракул реально проверяет заявленное)
- [ ] Тест проверяет то, что обещает имя/описание, а не суррогат. «Валидация формы» → инспекция реального payload, не только «кнопка активна». «Загрузка ещё» → реальная догрузка и сверка, не только клик. (🟠)
- [ ] Привязка к контенту структурная (наличие, непустота, `count > 0`, regex), чтобы тест пережил смену копирайта/цен — особенно для регрессов после фикса. (🟠)
- [ ] **Пороги соразмерны наблюдённым фактам.** Счётчик с кратным запасом (`>= 8` при 15 на странице, `>= 20 ссылок` при 40 в футере) переживёт потерю половины страницы — это оракул-плацебо. Где есть адресный якорь (собственный класс блока, ссылки в дочерний раздел, колонки футера) — ассертить его, а не общий счётчик по странице; порог ставить от замеренных значений на каждом окружении. Для каждого порога назвать дефект, который он ещё ловит, — и тот, который уже нет. (🟠)
- [ ] Оракул адекватен ограничению окружения: где UI не различает 404/5xx (одна заглушка на оба) — проверка сетевая, не «увидел текст ошибки». (🟠)
- [ ] **Оракул, выведенный из первого экземпляра коллекции, проверен на всех.** Раскладка/структура блока №1 не обязана совпадать с №3 (первая мозаика — колонка, третья — «1 + 2»; у первой карточки есть CTA-кнопка, у соседней нет). Обобщение по `first()` даёт либо ложное падение на корректной вёрстке, либо тест, зависящий от порядка контента; проверять инвариант, общий для всех экземпляров, а не свойство первого. (🟠)

### K. Правила вашего проекта (шаблон — заполните под свой репозиторий)
> У зрелого тест-репозитория всегда есть конвенции, которые не проверит ни один универсальный чеклист. Зафиксируйте их здесь или в `CLAUDE.md` проекта — тогда ревью будет ловить их нарушения. Типовые категории с примерами:

- [ ] **Кастомные фикстуры:** где импортировать `test` из кастомной фикстуры (`./fixtures/custom-test`) вместо `@playwright/test`, и какие обязательные проверки она даёт (напр. монитор сетевых ошибок, вызываемый в конце теста / в `afterEach`). (🟠)
- [ ] **Паттерны директорий:** чем отличаются правила smoke / regress / api сьютов — композиция vs фикстуры для POM, репортер-аннотации, testMatch/testIgnore, куда добавлять новые тесты. (🟠)
- [ ] **Окружения:** тест и POM проверены на всех целевых стендах, не только на одном (DOM на тест-стенде может отличаться от прода); известные особенности стендов зафиксированы списком. (🟠)
- [ ] **Skipped-гигиена:** тест не добавляет постоянных skipped в штатные прогоны; окружение-специфичное исключается конфигом (`testIgnore`/`testMatch`), runtime `test.skip` — только для динамических условий (фича-флаг, известный баг с тикетом). Итог прогона: passed = ок, failed = проблема, skipped = требует объяснения. (🟡)
- [ ] **Зависимости/конфиг:** не бампать версию `@playwright/test` и не добавлять зависимости без сверки с CI (Docker-образ, lock-файл); не трогать `playwright.config.ts` без необходимости. (🔴 если затронуто без запроса)

---

## Формат отчёта

```
## Ревью: <файлы / scope>

**Статический анализ:** typecheck ✅/❌ · lint ✅/❌ · (прогон: N/N pass, --repeat-each=5)

### 🔴 Blocker (N)
1. `path/to/spec.ts:42` — <что не так>.
   Почему: <ссылка на правило/категорию>.
   Фикс:
   ```ts
   // ❌ было / ✅ стало
   ```

### 🟠 Major (N)
…

### 🟡 Minor (N)
…

### ⚪ Nit (N)
…

### ❓ Под вопросом
- `path/to/spec.ts:42` - <решение, которое без контекста выглядит нарушением> - вопрос автору: <…>

### ✅ Что хорошо
- <что соответствует best practice — кратко>

### Вердикт
<Готов к коммиту / К доработке: список Blocker+Major> · <команда для прогона с правильным --project>
```

Правила:
- Сортировка строго по severity (Blocker → Nit). Внутри — по файлу/строке.
- Каждый Blocker/Major — с конкретным фиксом (сниппет `❌ было → ✅ стало`).
- Чисто в категории — пиши «чисто», не выдумывай.
- В конце — однострочный вердикт и команда запуска с верным `--project`.
- Вердикт заканчивается строкой для того, кто будет применять фиксы: одно изменение → typecheck/прогон → следующее, рабочий тест целиком не переписывать.

Files in this skill

  • SKILL.md29.6 KB
  • references/rules-catalog.md28.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…