Files
wiki/tasks/ui-test-skill/DEV_TASK.md
2026-05-13 00:20:05 +03:00

617 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DEV TASK: UI/UX Auto-Testing Skill
**Статус:** Ready for dev
**Проект:** ui-test-skill
**BRD:** `tasks/ui-test-skill/BRD.md`
---
## Цель
> Скилл для автономного UI/UX тестирования веб-приложений: запуск Chromium через Playwright, выполнение сценариев, скриншоты, анализ через vision-модель, отчёт.
## Архитектура
Playwright (Node.js) запускает Chromium headless shell напрямую (без OpenClaw browser plugin). Скрипт-раннер читает тест-кейсы из markdown, выполняет шаги, делает скриншоты. Стрим анализирует скриншоты через `image` tool и формирует отчёт.
```
SKILL.md (инструкции для Стрим)
scripts/run_tests.js (раннер) ← TEST_CASES_*.md
Playwright + Chromium headless shell
screenshots/ → Стрим анализирует через image tool
report.md (результат)
```
## Стек / Зависимости
- Node.js (уже есть: v24.14.0)
- Playwright (уже установлен: `npx playwright`)
- Chromium headless shell (уже скачан: `~/.cache/ms-playwright/chromium_headless_shell-1223/`)
- Системные .so библиотеки (уже извлечены: `~/chromium-libs/libs/`)
---
## Инфраструктура
| Параметр | Значение |
|----------|----------|
| Workspace | `/home/node/.openclaw/workspace/skills/ui-test/` |
| Chromium | `~/.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell` |
| LD_LIBRARY_PATH | `~/chromium-libs/libs/usr/lib/x86_64-linux-gnu:~/chromium-libs/libs/lib/x86_64-linux-gnu` |
| Screenshots | `tasks/{project}/reports/screenshots/` |
| Reports | `tasks/{project}/reports/` |
---
## Файловая карта
| Действие | Файл | Ответственность |
|----------|------|-----------------|
| Создать | `skills/ui-test/SKILL.md` | Инструкции для Стрим |
| Создать | `skills/ui-test/scripts/run_tests.js` | Раннер тестов (Playwright) |
| Создать | `skills/ui-test/scripts/parse_testcases.js` | Парсер markdown тест-кейсов |
| Создать | `skills/ui-test/scripts/package.json` | Зависимости (playwright) |
| Создать | `skills/ui-test/scripts/health_check.js` | Проверка готовности окружения |
| Создать | `skills/ui-test/examples/TEST_CASES_EXAMPLE.md` | Пример формата тест-кейсов |
---
## Задачи
### Task 1: Health check скрипт
**Файлы:**
- Создать: `skills/ui-test/scripts/health_check.js`
**Шаги:**
- [ ] **1.1** Создать скрипт проверки окружения:
```javascript
#!/usr/bin/env node
// health_check.js — проверяет что всё готово для UI-тестирования
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const CHROMIUM_PATH = path.join(
process.env.HOME,
'.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell'
);
const LIB_PATH = path.join(process.env.HOME, 'chromium-libs/libs/usr/lib/x86_64-linux-gnu');
const checks = [];
// 1. Chromium binary exists
if (fs.existsSync(CHROMIUM_PATH)) {
checks.push({ name: 'Chromium binary', status: 'OK' });
} else {
checks.push({ name: 'Chromium binary', status: 'FAIL', detail: `Not found: ${CHROMIUM_PATH}` });
}
// 2. Libraries exist
if (fs.existsSync(LIB_PATH)) {
const libs = fs.readdirSync(LIB_PATH).filter(f => f.endsWith('.so') || f.includes('.so.'));
checks.push({ name: 'Shared libraries', status: libs.length > 20 ? 'OK' : 'FAIL', detail: `${libs.length} .so files` });
} else {
checks.push({ name: 'Shared libraries', status: 'FAIL', detail: `Not found: ${LIB_PATH}` });
}
// 3. Chromium launches
try {
const env = { ...process.env, LD_LIBRARY_PATH: `${LIB_PATH}:${path.join(process.env.HOME, 'chromium-libs/libs/lib/x86_64-linux-gnu')}` };
execSync(`"${CHROMIUM_PATH}" --headless --disable-gpu --no-sandbox --dump-dom about:blank`, { env, timeout: 10000, stdio: 'pipe' });
checks.push({ name: 'Chromium launches', status: 'OK' });
} catch (e) {
checks.push({ name: 'Chromium launches', status: 'FAIL', detail: e.message.slice(0, 100) });
}
// 4. Playwright available
try {
require('playwright-core');
checks.push({ name: 'Playwright module', status: 'OK' });
} catch {
checks.push({ name: 'Playwright module', status: 'FAIL', detail: 'npm install playwright-core needed' });
}
// Output
const allOk = checks.every(c => c.status === 'OK');
console.log(allOk ? '✅ All checks passed' : '❌ Some checks failed');
checks.forEach(c => {
const icon = c.status === 'OK' ? '✓' : '✗';
console.log(` ${icon} ${c.name}: ${c.status}${c.detail ? ' — ' + c.detail : ''}`);
});
process.exit(allOk ? 0 : 1);
```
- [ ] **1.2** Проверить:
```bash
cd /home/node/.openclaw/workspace/skills/ui-test/scripts && node health_check.js
# Ожидаемый результат: ✅ All checks passed
```
**Критерий готовности:** Скрипт запускается, все 4 проверки зелёные.
---
### Task 2: Парсер тест-кейсов
**Файлы:**
- Создать: `skills/ui-test/scripts/parse_testcases.js`
**Шаги:**
- [ ] **2.1** Создать парсер markdown → JSON:
Формат входного markdown:
```markdown
### TC-XX — Название теста
**Тип:** ui
**Viewport:** desktop | mobile | both
**URL:** https://example.com/
**Шаги:**
1. navigate: {url}
2. wait: 3000
3. click: "#button-id"
4. screenshot: "step-name"
5. check-visual: "Описание что проверяем"
**Визуальные критерии:**
- Нет пустых белых/чёрных областей
- Текст читаем
```
Формат выхода (JSON):
```json
[
{
"id": "TC-XX",
"name": "Название теста",
"type": "ui",
"viewport": "both",
"url": "https://example.com/",
"steps": [
{ "action": "navigate", "value": "{url}" },
{ "action": "wait", "value": 3000 },
{ "action": "click", "value": "#button-id" },
{ "action": "screenshot", "value": "step-name" },
{ "action": "check-visual", "value": "Описание что проверяем" }
],
"criteria": ["Нет пустых белых/чёрных областей", "Текст читаем"]
}
]
```
Парсер должен:
- Читать файл markdown
- Извлекать только тест-кейсы с `**Тип:** ui`
- Игнорировать тесты без типа `ui` (они для curl/grep)
- Выводить JSON в stdout
```bash
node parse_testcases.js /path/to/TEST_CASES.md
```
- [ ] **2.2** Проверить на примере:
```bash
node parse_testcases.js ../examples/TEST_CASES_EXAMPLE.md | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.length + ' tests parsed')"
# Ожидаемый результат: N tests parsed (N > 0)
```
**Критерий готовности:** Парсер корректно извлекает UI тест-кейсы из markdown в JSON.
---
### Task 3: Раннер тестов (основной скрипт)
**Файлы:**
- Создать: `skills/ui-test/scripts/run_tests.js`
- Создать: `skills/ui-test/scripts/package.json`
**Шаги:**
- [ ] **3.1** Создать `package.json`:
```json
{
"name": "ui-test-runner",
"version": "1.0.0",
"private": true,
"dependencies": {
"playwright-core": "^1.52.0"
}
}
```
- [ ] **3.2** Установить зависимости:
```bash
cd /home/node/.openclaw/workspace/skills/ui-test/scripts && npm install
```
- [ ] **3.3** Создать `run_tests.js`:
Скрипт должен:
1. Принимать аргументы: `node run_tests.js <test-cases.md> <output-dir>`
2. Парсить тест-кейсы через `parse_testcases.js`
3. Запускать Chromium через playwright-core с правильным `LD_LIBRARY_PATH`
4. Для каждого теста:
- Установить viewport (desktop: 1280×720, mobile: 375×812)
- Выполнить шаги: navigate, wait, click, scroll, screenshot
- Сохранить скриншоты в `<output-dir>/screenshots/`
5. Сформировать JSON-отчёт: `<output-dir>/results.json`
Ключевые моменты реализации:
```javascript
const { chromium } = require('playwright-core');
const path = require('path');
const CHROMIUM_PATH = path.join(
process.env.HOME,
'.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell'
);
const LIB_PATH = [
path.join(process.env.HOME, 'chromium-libs/libs/usr/lib/x86_64-linux-gnu'),
path.join(process.env.HOME, 'chromium-libs/libs/lib/x86_64-linux-gnu')
].join(':');
async function launchBrowser(viewport) {
const browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
env: { ...process.env, LD_LIBRARY_PATH: LIB_PATH }
});
const context = await browser.newContext({
viewport: viewport === 'mobile'
? { width: 375, height: 812 }
: { width: 1280, height: 720 },
deviceScaleFactor: viewport === 'mobile' ? 2 : 1,
isMobile: viewport === 'mobile',
userAgent: viewport === 'mobile'
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15'
: undefined
});
return { browser, context };
}
async function executeStep(page, step, screenshotDir, testId) {
switch (step.action) {
case 'navigate':
await page.goto(step.value, { waitUntil: 'networkidle', timeout: 30000 });
break;
case 'wait':
await page.waitForTimeout(parseInt(step.value));
break;
case 'click':
await page.click(step.value, { timeout: 5000 });
break;
case 'scroll':
await page.evaluate((px) => window.scrollBy(0, parseInt(px)), step.value);
break;
case 'screenshot':
const filename = `${testId}-${step.value}.png`;
await page.screenshot({ path: path.join(screenshotDir, filename), fullPage: false });
return { screenshot: filename };
break;
case 'check-visual':
// Скриншот для визуальной проверки (будет анализироваться Стрим через image tool)
const checkFile = `${testId}-check-${Date.now()}.png`;
await page.screenshot({ path: path.join(screenshotDir, checkFile), fullPage: false });
return { screenshot: checkFile, checkDescription: step.value };
break;
case 'resize':
const [w, h] = step.value.split('x').map(Number);
await page.setViewportSize({ width: w, height: h });
break;
}
return null;
}
```
Формат `results.json`:
```json
{
"timestamp": "2026-05-12T21:00:00Z",
"testFile": "TEST_CASES_TERRAIN.md",
"results": [
{
"id": "TC-07",
"name": "Кнопка terrain",
"viewport": "desktop",
"status": "completed",
"screenshots": ["TC-07-desktop-initial.png", "TC-07-desktop-after-click.png"],
"checks": [
{ "description": "Попап виден", "screenshot": "TC-07-desktop-check-1715...png" }
],
"error": null
}
]
}
```
- [ ] **3.4** Проверить на тестовом URL:
```bash
cd /home/node/.openclaw/workspace/skills/ui-test/scripts
node run_tests.js ../examples/TEST_CASES_EXAMPLE.md /tmp/ui-test-output
ls /tmp/ui-test-output/screenshots/
cat /tmp/ui-test-output/results.json | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.results.length + ' tests completed')"
```
**Критерий готовности:** Раннер запускает Chromium, выполняет шаги, сохраняет скриншоты и results.json.
---
### Task 4: Пример тест-кейсов
**Файлы:**
- Создать: `skills/ui-test/examples/TEST_CASES_EXAMPLE.md`
**Шаги:**
- [ ] **4.1** Создать пример с 3 тест-кейсами для enduro-trails:
```markdown
# UI Test Cases: Enduro Trails (Example)
### TC-UI-01 — Загрузка карты
**Тип:** ui
**Viewport:** both
**URL:** https://openclaw.mva154.duckdns.org/enduro/
**Шаги:**
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 3000
3. screenshot: "initial-load"
4. check-visual: "Карта загружена, тайлы отрисованы, нет белых пустот. Toolbar виден внизу."
**Визуальные критерии:**
- Карта занимает весь экран
- Нет белых/серых незагруженных областей
- Toolbar с кнопками виден
---
### TC-UI-02 — Переключение темы
**Тип:** ui
**Viewport:** desktop
**URL:** https://openclaw.mva154.duckdns.org/enduro/
**Шаги:**
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 3000
3. screenshot: "before-theme-switch"
4. click: "#theme-toggle"
5. wait: 2000
6. screenshot: "after-theme-switch"
7. check-visual: "Тема переключилась: фон карты изменился (светлый↔тёмный), кнопки toolbar сменили цвет"
**Визуальные критерии:**
- Скриншоты before/after визуально отличаются
- Карта не пустая после переключения
- Toolbar адаптировался к новой теме
---
### TC-UI-03 — Terrain попап (мобильный)
**Тип:** ui
**Viewport:** mobile
**URL:** https://openclaw.mva154.duckdns.org/enduro/
**Шаги:**
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 3000
3. click: "#terrain-btn"
4. wait: 1000
5. screenshot: "terrain-popup-mobile"
6. check-visual: "Попап terrain виден полностью, не обрезан снизу. Чекбоксы кликабельного размера (>44px)."
**Визуальные критерии:**
- Попап не выходит за границы экрана
- Чекбоксы достаточного размера для тапа
- Текст читаем
```
**Критерий готовности:** Файл существует, парсер корректно извлекает 3 теста.
---
### Task 5: SKILL.md
**Файлы:**
- Создать: `skills/ui-test/SKILL.md`
**Шаги:**
- [ ] **5.1** Создать SKILL.md с полными инструкциями:
```markdown
---
name: ui-test
description: "Автономное UI/UX тестирование веб-приложений через Playwright + vision analysis. Запуск тестов, скриншоты, визуальный анализ, отчёт."
---
# UI/UX Auto-Testing Skill
## Когда использовать
- После деплоя новой фичи — проверить что ничего не сломалось
- По запросу Славы — "проверь UI"
- Перед релизом фазы — регрессионный прогон всех тестов
## Порядок тестирования
1. Unit/integration тесты (curl/grep, API)
2. UI/UX тесты (этот скилл) — ПОСЛЕДНИМИ
## Быстрый старт
### 1. Проверить готовность
\`\`\`bash
cd /home/node/.openclaw/workspace/skills/ui-test/scripts && node health_check.js
\`\`\`
Все проверки должны быть ✅. Если нет — см. раздел "Установка".
### 2. Запустить тесты
\`\`\`bash
cd /home/node/.openclaw/workspace/skills/ui-test/scripts
node run_tests.js /path/to/TEST_CASES.md /path/to/output-dir
\`\`\`
Пример для enduro-trails:
\`\`\`bash
node run_tests.js \
/home/node/.openclaw/workspace/tasks/enduro-trails/TEST_CASES_UI.md \
/home/node/.openclaw/workspace/tasks/enduro-trails/reports
\`\`\`
### 3. Проанализировать скриншоты
После запуска раннера — проанализировать скриншоты через `image` tool:
Для каждого check из `results.json`:
- Открыть скриншот через image tool
- Промпт: "Ты — QA-инженер. Проанализируй скриншот. {check.description}. Есть ли визуальные проблемы? Ответь: pass/fail + описание проблем если есть."
- Записать результат в отчёт
### 4. Сформировать отчёт
Создать `reports/ui-test-YYYY-MM-DD.md`:
\`\`\`markdown
# UI Test Report: {project}
**Дата:** {date}
**Тесты:** {total} | ✅ {passed} | ❌ {failed}
| # | Тест | Desktop | Mobile | Проблемы |
|---|------|---------|--------|----------|
| TC-XX | Название | ✅/❌ | ✅/❌ | описание |
\`\`\`
### 5. Отправить результат Славе
Всегда отправлять результат — даже если всё зелёное.
## Формат тест-кейсов
Файл: `TEST_CASES_UI.md` (или секция в общем TEST_CASES файле)
\`\`\`markdown
### TC-XX — Название
**Тип:** ui
**Viewport:** desktop | mobile | both
**URL:** https://...
**Шаги:**
1. navigate: {url}
2. wait: {ms}
3. click: "{selector}"
4. scroll: {pixels}
5. resize: {width}x{height}
6. screenshot: "{name}"
7. check-visual: "{описание проверки}"
**Визуальные критерии:**
- Критерий 1
- Критерий 2
\`\`\`
### Доступные действия
| Действие | Формат | Описание |
|----------|--------|----------|
| navigate | `navigate: {url}` | Перейти на URL |
| wait | `wait: {ms}` | Подождать N миллисекунд |
| click | `click: "{selector}"` | Кликнуть по CSS-селектору |
| scroll | `scroll: {pixels}` | Прокрутить вниз на N пикселей |
| resize | `resize: {w}x{h}` | Изменить viewport |
| screenshot | `screenshot: "{name}"` | Сделать скриншот |
| check-visual | `check-visual: "{desc}"` | Скриншот + пометка для vision-анализа |
## Установка (если health_check падает)
### Chromium headless shell
\`\`\`bash
npx --yes playwright install chromium
\`\`\`
### Системные библиотеки (без root)
\`\`\`bash
# Библиотеки уже извлечены в ~/chromium-libs/libs/
# Если нет — скачать deb-пакеты и извлечь через dpkg-deb:
mkdir -p ~/chromium-libs && cd ~/chromium-libs
# ... (список URL в BRD)
for deb in *.deb; do dpkg-deb -x "$deb" libs/; done
\`\`\`
### Node.js зависимости
\`\`\`bash
cd /home/node/.openclaw/workspace/skills/ui-test/scripts && npm install
\`\`\`
## Ограничения
- Chromium headless — нет GPU рендеринга (WebGL карты могут выглядеть иначе)
- Vision-анализ субъективен — false positives возможны
- MapLibre тайлы грузятся по сети — нужен wait 3-5 сек после navigate
- Мобильный viewport эмулирует размер, но не touch-события
```
**Критерий готовности:** SKILL.md содержит полные инструкции: быстрый старт, формат тестов, установка, ограничения.
---
## Проверка (Acceptance)
| # | Проверка | Команда / Действие | Ожидаемый результат |
|---|----------|-------------------|---------------------|
| 1 | Health check | `node health_check.js` | ✅ All checks passed |
| 2 | Парсер | `node parse_testcases.js examples/TEST_CASES_EXAMPLE.md` | Валидный JSON, 3 теста |
| 3 | Раннер desktop | `node run_tests.js examples/TEST_CASES_EXAMPLE.md /tmp/test1` | screenshots/ содержит PNG файлы |
| 4 | Раннер mobile | Тест с viewport: mobile | Скриншот 375×812 |
| 5 | results.json | `cat /tmp/test1/results.json` | Валидный JSON со статусами |
| 6 | Реальный URL | Тест на openclaw.mva154.duckdns.org/enduro/ | Скриншот с картой (не пустой) |
---
## Ограничения и контекст
- ⚠️ Chromium запускается с `LD_LIBRARY_PATH` — обязательно передавать env в `chromium.launch()`
- ⚠️ `--no-sandbox` обязателен (контейнер без root)
- ⚠️ `--disable-dev-shm-usage` — /dev/shm маленький в контейнере
- ⚠️ MapLibre карта грузит тайлы по сети — wait минимум 3000ms после navigate
- ⚠️ Headless shell не поддерживает `--headless=new` — использовать просто `headless: true`
- 🚫 НЕ использовать OpenClaw browser tool — работаем через Playwright напрямую
- 🚫 НЕ устанавливать ничего через apt/sudo — всё уже есть
---
## Деплой-чеклист
- [ ] `scripts/health_check.js` — работает
- [ ] `scripts/parse_testcases.js` — парсит markdown
- [ ] `scripts/run_tests.js` — запускает тесты, делает скриншоты
- [ ] `scripts/package.json` + `node_modules/` — playwright-core установлен
- [ ] `examples/TEST_CASES_EXAMPLE.md` — пример тестов
- [ ] `SKILL.md` — полные инструкции
- [ ] Тест на реальном URL — скриншот не пустой
---
*Создано: 2026-05-12 | Автор ТЗ: Стрим | Исполнитель: Dev-агент*