auto-sync: 2026-05-04 12:20:01
This commit is contained in:
215
tasks/ui-testing/PROJECT.md
Normal file
215
tasks/ui-testing/PROJECT.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# UI Testing Infrastructure 🧪
|
||||
|
||||
> Инфраструктура для автоматизированного UI-тестирования веб-приложений с визуальной проверкой скриншотов
|
||||
|
||||
**Статус:** planning
|
||||
**Старт:** 2026-05-04
|
||||
**Автор:** Стрим 🌊
|
||||
|
||||
---
|
||||
|
||||
## Проблема
|
||||
|
||||
При тестировании Enduro Trails Фаза 3 столкнулись с тем, что:
|
||||
- Playwright/Puppeteer не запускаются в OpenClaw-контейнере (нет системных библиотек: libglib, libnss, libatk и др.)
|
||||
- SSH-бинарник в контейнере требует glibc 2.38+, а в контейнере 2.36
|
||||
- Внешние screenshot-сервисы требуют API-ключи
|
||||
- Без скриншотов невозможно проверить визуальные тест-кейсы: hover, drag, цвета, layout
|
||||
|
||||
Из 56 тест-кейсов Фазы 3 только ~20 можно проверить через API. Остальные 36 требуют браузера.
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Создать **переиспользуемую инфраструктуру** для UI-тестирования любых веб-приложений проекта:
|
||||
- Запуск браузера (headless Chromium)
|
||||
- Скриншоты на каждом шаге
|
||||
- Анализ скриншотов через vision-модель (Qwen 3.6 Plus)
|
||||
- Структурированный отчёт с результатами
|
||||
|
||||
---
|
||||
|
||||
## Варианты реализации
|
||||
|
||||
### Вариант A: Chromium на сервере mva154 (рекомендуемый)
|
||||
|
||||
**Идея:** установить Chromium на mva154, запускать тесты там через SSH, скриншоты копировать обратно.
|
||||
|
||||
**Плюсы:**
|
||||
- Полный контроль над окружением
|
||||
- Нет ограничений контейнера
|
||||
- Один раз настроил — работает всегда
|
||||
- Тесты гоняются рядом с приложением (нет сетевых задержек)
|
||||
|
||||
**Минусы:**
|
||||
- Нужна установка на сервере (apt install chromium-browser)
|
||||
- Зависит от доступности mva154
|
||||
|
||||
**Реализация:**
|
||||
```bash
|
||||
# На mva154:
|
||||
sudo apt-get install -y chromium-browser nodejs npm
|
||||
npm install -g puppeteer-core
|
||||
|
||||
# Скрипт теста запускается через ssh_exec.sh:
|
||||
ssh_exec.sh --host mva154 --cmd "node /home/slin/ui-tests/test.js" --timeout 120
|
||||
# Скриншоты копируются обратно через scp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Вариант B: Docker-контейнер с Chromium на mva154
|
||||
|
||||
**Идея:** отдельный Docker-контейнер `ui-tester` с предустановленным Playwright.
|
||||
|
||||
**Плюсы:**
|
||||
- Изолированное окружение
|
||||
- Воспроизводимо
|
||||
- Можно запускать по требованию
|
||||
|
||||
**Минусы:**
|
||||
- Нужно место на диске (~1.5 GB для Playwright + Chromium)
|
||||
- Дополнительный контейнер
|
||||
|
||||
**Dockerfile:**
|
||||
```dockerfile
|
||||
FROM mcr.microsoft.com/playwright:v1.44.0-jammy
|
||||
WORKDIR /tests
|
||||
COPY package.json .
|
||||
RUN npm install
|
||||
```
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
docker run --rm -v /home/slin/ui-tests:/tests ui-tester node test.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Вариант C: GitHub Actions / CI (для будущего)
|
||||
|
||||
**Идея:** тесты запускаются в CI при каждом деплое.
|
||||
|
||||
**Плюсы:** автоматически, не требует ручного запуска
|
||||
**Минусы:** нужен GitHub repo, сложнее настроить
|
||||
|
||||
---
|
||||
|
||||
## Требования к инфраструктуре
|
||||
|
||||
### R-01: Запуск браузера
|
||||
- Headless Chromium (без GUI)
|
||||
- Поддержка viewport: desktop (1280×800) и mobile (375×667)
|
||||
- Поддержка JavaScript (SPA-приложения)
|
||||
- Ожидание `networkidle` перед скриншотом
|
||||
|
||||
### R-02: Управление тестами
|
||||
- Тесты описываются как JS-скрипты (Playwright API)
|
||||
- Каждый тест: precondition → steps → assertions → screenshot
|
||||
- Поддержка `waitForSelector`, `waitForFunction`, `click`, `hover`, `drag`
|
||||
- Debounce-действия (ждать N мс после клика)
|
||||
|
||||
### R-03: Скриншоты
|
||||
- PNG, полная страница или viewport
|
||||
- Именование: `TC-ID-step-description.png`
|
||||
- Хранение: `/home/node/.openclaw/workspace/tasks/<project>/screenshots/`
|
||||
- Автоматическое создание директории
|
||||
|
||||
### R-04: Анализ скриншотов через vision
|
||||
- После каждого скриншота — опциональный вызов image-tool с Qwen 3.6 Plus
|
||||
- Промпт для анализа: что видно на скрине, соответствует ли ожиданиям
|
||||
- Результат анализа включается в отчёт
|
||||
|
||||
### R-05: Отчёт
|
||||
- Markdown-файл с результатами
|
||||
- Структура: PASSED / FAILED / BLOCKED
|
||||
- Для каждого FAILED: скриншот + описание бага
|
||||
- Итоговая статистика
|
||||
|
||||
### R-06: Интеграция с OpenClaw
|
||||
- Запуск через `sessions_spawn` (dev-агент)
|
||||
- Скриншоты доступны в workspace для анализа через `image` tool
|
||||
- Отчёт сохраняется в `tasks/<project>/reports/`
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
OpenClaw (Стрим)
|
||||
│
|
||||
├── sessions_spawn(dev) → запуск тестов
|
||||
│ │
|
||||
│ └── ssh_exec.sh --host mva154
|
||||
│ │
|
||||
│ └── node test.js (Playwright на mva154)
|
||||
│ │
|
||||
│ ├── Chromium headless
|
||||
│ ├── Скриншоты → /tmp/screenshots/
|
||||
│ └── Результаты → /tmp/test_results.json
|
||||
│
|
||||
├── scp скриншоты → workspace/screenshots/
|
||||
│
|
||||
└── image(screenshot, model=qwen3.6) → визуальный анализ
|
||||
│
|
||||
└── Финальный отчёт TEST_REPORT.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
tasks/ui-testing/
|
||||
├── PROJECT.md ← этот файл
|
||||
├── README.md ← как запустить
|
||||
├── scripts/
|
||||
│ ├── setup-mva154.sh ← установка Chromium на mva154
|
||||
│ ├── run-tests.sh ← запуск тестов (wrapper)
|
||||
│ └── analyze-screenshots.js ← анализ через vision API
|
||||
├── tests/
|
||||
│ ├── enduro-phase3.js ← тесты Enduro Trails Фаза 3
|
||||
│ └── template.js ← шаблон для новых тестов
|
||||
└── reports/
|
||||
└── .gitkeep
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## План реализации
|
||||
|
||||
### Этап 1: Установка окружения на mva154
|
||||
- [ ] Установить chromium-browser через apt
|
||||
- [ ] Установить Node.js 20+ (если нет)
|
||||
- [ ] Установить puppeteer-core глобально
|
||||
- [ ] Проверить: `chromium-browser --version`
|
||||
|
||||
### Этап 2: Базовый тест-раннер
|
||||
- [ ] Написать `scripts/run-tests.sh` — wrapper для запуска через ssh_exec.sh
|
||||
- [ ] Написать `tests/template.js` — шаблон теста с Puppeteer
|
||||
- [ ] Проверить: скриншот главной страницы Enduro Trails
|
||||
|
||||
### Этап 3: Тесты Enduro Trails Фаза 3
|
||||
- [ ] Перенести все 56 тест-кейсов из TEST_CASES_PHASE3.md в `tests/enduro-phase3.js`
|
||||
- [ ] Запустить полный прогон
|
||||
- [ ] Скопировать скриншоты в workspace
|
||||
|
||||
### Этап 4: Визуальный анализ
|
||||
- [ ] Для каждого скриншота — вызов `image` tool с Qwen 3.6 Plus
|
||||
- [ ] Промпты для каждого типа проверки (карточки, полоска, hover и т.д.)
|
||||
- [ ] Итоговый отчёт
|
||||
|
||||
### Этап 5: Переиспользование
|
||||
- [ ] Документировать как добавить тесты для нового проекта
|
||||
- [ ] Шаблон тест-скрипта
|
||||
|
||||
---
|
||||
|
||||
## Приоритет
|
||||
|
||||
**Первый запуск:** Enduro Trails Фаза 3 — 36 непроверенных UI тест-кейсов ждут.
|
||||
|
||||
---
|
||||
|
||||
*Проект создан после того как Playwright не запустился в OpenClaw-контейнере при тестировании Enduro Trails Фаза 3 (2026-05-04).*
|
||||
83
tasks/ui-testing/README.md
Normal file
83
tasks/ui-testing/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# README: UI Testing Infrastructure
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1. Установить окружение на mva154 (один раз)
|
||||
|
||||
```bash
|
||||
SKILL=~/.openclaw/skills/installer/scripts
|
||||
$SKILL/ssh_exec.sh --host mva154 --cmd "bash -s" --timeout 300 < tasks/ui-testing/scripts/setup-mva154.sh
|
||||
```
|
||||
|
||||
### 2. Запустить тесты
|
||||
|
||||
```bash
|
||||
bash tasks/ui-testing/scripts/run-tests.sh enduro-phase3 enduro-trails
|
||||
```
|
||||
|
||||
Скриншоты появятся в `tasks/enduro-trails/screenshots/`.
|
||||
|
||||
### 3. Проанализировать скриншоты
|
||||
|
||||
Стрим анализирует каждый скриншот через `image` tool с моделью Qwen 3.6 Plus.
|
||||
|
||||
---
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
tasks/ui-testing/
|
||||
├── PROJECT.md — описание проекта, требования, варианты реализации
|
||||
├── README.md — этот файл
|
||||
├── scripts/
|
||||
│ ├── setup-mva154.sh — установка Chromium + puppeteer на mva154
|
||||
│ └── run-tests.sh — запуск тестов, копирование скриншотов
|
||||
└── tests/
|
||||
├── template.js — шаблон теста (копировать для нового проекта)
|
||||
└── enduro-phase3.js — тесты Enduro Trails Фаза 3 (56 TC)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Написать новый тест
|
||||
|
||||
1. Скопировать `tests/template.js` → `tests/my-project.js`
|
||||
2. Заменить `CONFIG.url` на нужный URL
|
||||
3. Добавить тест-кейсы в функцию `runTests(page)`
|
||||
4. Запустить: `bash scripts/run-tests.sh my-project my-project`
|
||||
|
||||
### API хелперов
|
||||
|
||||
```js
|
||||
// Скриншот
|
||||
const snap = await screenshot(page, 'TC-01-step-name');
|
||||
|
||||
// Проверки
|
||||
pass('TC-01', 'описание что прошло');
|
||||
fail('TC-01', 'что не так', snap); // snap — путь к скриншоту
|
||||
blocked('TC-01', 'почему заблокировано');
|
||||
|
||||
// Действия
|
||||
await waitAndClick(page, '#btn-route');
|
||||
await sleep(500); // ждать 500ms
|
||||
const text = await getText(page, '#route-status');
|
||||
const visible = await isVisible(page, '#route-panel');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Требования к окружению
|
||||
|
||||
| Компонент | Версия | Где |
|
||||
|-----------|--------|-----|
|
||||
| Node.js | 20+ | mva154 |
|
||||
| chromium-browser | любая | mva154 |
|
||||
| puppeteer-core | 22+ | mva154 (/home/slin/ui-tests/) |
|
||||
|
||||
---
|
||||
|
||||
## Известные ограничения
|
||||
|
||||
- **Playwright не работает в OpenClaw-контейнере** — нет системных библиотек (libglib, libnss и др.)
|
||||
- **SSH-бинарник в контейнере** требует glibc 2.38+, контейнер на 2.36 → используем installer skill
|
||||
- **MapLibre GL** рендерит карту через WebGL — скриншоты карты могут быть пустыми в headless без GPU. Решение: `--use-gl=swiftshader` или проверять UI-элементы вокруг карты, не саму карту
|
||||
64
tasks/ui-testing/scripts/run-tests.sh
Normal file
64
tasks/ui-testing/scripts/run-tests.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# run-tests.sh — запуск UI-тестов через ssh_exec.sh
|
||||
# Использование: bash tasks/ui-testing/scripts/run-tests.sh <test-file> [project-name]
|
||||
#
|
||||
# Пример:
|
||||
# bash tasks/ui-testing/scripts/run-tests.sh enduro-phase3 enduro-trails
|
||||
|
||||
set -e
|
||||
|
||||
SKILL=~/.openclaw/skills/installer/scripts
|
||||
TEST_NAME=${1:-"template"}
|
||||
PROJECT=${2:-"ui-testing"}
|
||||
WORKSPACE=/home/node/.openclaw/workspace
|
||||
TESTS_DIR=$WORKSPACE/tasks/ui-testing/tests
|
||||
SCREENSHOTS_DIR=$WORKSPACE/tasks/$PROJECT/screenshots
|
||||
REPORTS_DIR=$WORKSPACE/tasks/$PROJECT/reports
|
||||
REMOTE_DIR=/home/slin/ui-tests
|
||||
|
||||
echo "==> Запуск UI-тестов: $TEST_NAME (проект: $PROJECT)"
|
||||
echo "==> Скриншоты: $SCREENSHOTS_DIR"
|
||||
|
||||
mkdir -p "$SCREENSHOTS_DIR" "$REPORTS_DIR"
|
||||
|
||||
# 1. Копируем тест на сервер
|
||||
echo "==> Копируем тест на mva154..."
|
||||
TEST_CONTENT=$(cat "$TESTS_DIR/${TEST_NAME}.js")
|
||||
$SKILL/ssh_exec.sh --host mva154 --cmd "mkdir -p $REMOTE_DIR/tests $REMOTE_DIR/screenshots && cat > $REMOTE_DIR/tests/${TEST_NAME}.js << 'ENDOFTEST'
|
||||
$TEST_CONTENT
|
||||
ENDOFTEST" --timeout 30
|
||||
|
||||
# 2. Запускаем тест
|
||||
echo "==> Запускаем тест на сервере..."
|
||||
CHROME_BIN=$($SKILL/ssh_exec.sh --host mva154 --cmd "which chromium-browser 2>/dev/null || which chromium 2>/dev/null || echo /usr/bin/chromium-browser" --timeout 10 | tail -1)
|
||||
|
||||
$SKILL/ssh_exec.sh --host mva154 --cmd "
|
||||
cd $REMOTE_DIR && \
|
||||
CHROME_BIN=$CHROME_BIN \
|
||||
SCREENSHOTS_DIR=$REMOTE_DIR/screenshots \
|
||||
RESULTS_FILE=$REMOTE_DIR/results.json \
|
||||
node tests/${TEST_NAME}.js 2>&1
|
||||
" --timeout 180
|
||||
|
||||
# 3. Копируем скриншоты обратно
|
||||
echo "==> Копируем скриншоты..."
|
||||
SCREENSHOTS=$($SKILL/ssh_exec.sh --host mva154 --cmd "ls $REMOTE_DIR/screenshots/*.png 2>/dev/null || echo ''" --timeout 10)
|
||||
|
||||
if [ -n "$SCREENSHOTS" ]; then
|
||||
for f in $SCREENSHOTS; do
|
||||
FNAME=$(basename "$f")
|
||||
$SKILL/ssh_exec.sh --host mva154 --cmd "cat $f | base64" --timeout 30 | base64 -d > "$SCREENSHOTS_DIR/$FNAME"
|
||||
echo " 📸 $FNAME"
|
||||
done
|
||||
echo "==> Скриншоты сохранены: $SCREENSHOTS_DIR"
|
||||
else
|
||||
echo "==> Скриншоты не найдены"
|
||||
fi
|
||||
|
||||
# 4. Копируем результаты
|
||||
RESULTS=$($SKILL/ssh_exec.sh --host mva154 --cmd "cat $REMOTE_DIR/results.json 2>/dev/null || echo '{}'" --timeout 10)
|
||||
echo "$RESULTS" > "$REPORTS_DIR/${TEST_NAME}-results.json"
|
||||
echo "==> Результаты: $REPORTS_DIR/${TEST_NAME}-results.json"
|
||||
|
||||
echo ""
|
||||
echo "✅ Готово. Скриншоты в: $SCREENSHOTS_DIR"
|
||||
58
tasks/ui-testing/scripts/setup-mva154.sh
Normal file
58
tasks/ui-testing/scripts/setup-mva154.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# setup-mva154.sh — установка UI-тестового окружения на mva154
|
||||
# Запускать через: ssh_exec.sh --host mva154 --cmd "bash /home/slin/ui-tests/scripts/setup-mva154.sh" --timeout 300
|
||||
|
||||
set -e
|
||||
|
||||
echo "==> Проверяем Node.js..."
|
||||
node --version || { echo "Node.js не найден, устанавливаем..."; curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -S bash -; sudo apt-get install -y nodejs; }
|
||||
|
||||
echo "==> Проверяем Chromium..."
|
||||
which chromium-browser || which chromium || {
|
||||
echo "Chromium не найден, устанавливаем..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y chromium-browser
|
||||
}
|
||||
|
||||
CHROME_BIN=$(which chromium-browser 2>/dev/null || which chromium 2>/dev/null)
|
||||
echo "==> Chromium: $CHROME_BIN"
|
||||
$CHROME_BIN --version
|
||||
|
||||
echo "==> Создаём директории..."
|
||||
mkdir -p /home/slin/ui-tests/{scripts,tests,screenshots,reports}
|
||||
|
||||
echo "==> Устанавливаем puppeteer-core..."
|
||||
cd /home/slin/ui-tests
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "ui-tests",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"puppeteer-core": "^22.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
npm install --quiet
|
||||
|
||||
echo "==> Проверочный тест..."
|
||||
node -e "
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const CHROME = process.env.CHROME_BIN || '$(which chromium-browser 2>/dev/null || which chromium 2>/dev/null)';
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROME,
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
console.log('Browser OK:', await browser.version());
|
||||
await browser.close();
|
||||
})().catch(e => { console.error('FAIL:', e.message); process.exit(1); });
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "✅ Окружение готово!"
|
||||
echo " Node: $(node --version)"
|
||||
echo " Chrome: $($CHROME_BIN --version)"
|
||||
echo " Dir: /home/slin/ui-tests/"
|
||||
451
tasks/ui-testing/tests/enduro-phase3.js
Normal file
451
tasks/ui-testing/tests/enduro-phase3.js
Normal file
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* enduro-phase3.js — UI тесты Enduro Trails Фаза 3
|
||||
* 56 тест-кейсов из TEST_CASES_PHASE3.md
|
||||
*
|
||||
* Запуск:
|
||||
* CHROME_BIN=/usr/bin/chromium-browser \
|
||||
* TEST_URL=https://openclaw.mva154.duckdns.org/enduro/ \
|
||||
* node enduro-phase3.js
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG = {
|
||||
url: process.env.TEST_URL || 'https://openclaw.mva154.duckdns.org/enduro/',
|
||||
chromeBin: process.env.CHROME_BIN || '/usr/bin/chromium-browser',
|
||||
screenshotsDir: process.env.SCREENSHOTS_DIR || '/tmp/enduro-screenshots',
|
||||
resultsFile: process.env.RESULTS_FILE || '/tmp/enduro-results.json',
|
||||
viewportDesktop: { width: 1280, height: 800 },
|
||||
viewportMobile: { width: 375, height: 667 },
|
||||
};
|
||||
|
||||
const results = [];
|
||||
function pass(id, note) { results.push({ id, status: 'PASS', note }); console.log(`✅ ${id}: ${note}`); }
|
||||
function fail(id, note, snap) { results.push({ id, status: 'FAIL', note, screenshot: snap }); console.log(`❌ ${id}: ${note}`); }
|
||||
function blocked(id, note) { results.push({ id, status: 'BLOCKED', note }); console.log(`⚠️ ${id}: ${note}`); }
|
||||
|
||||
async function screenshot(page, name) {
|
||||
const p = path.join(CONFIG.screenshotsDir, `${name}.png`);
|
||||
await page.screenshot({ path: p });
|
||||
console.log(` 📸 ${name}.png`);
|
||||
return p;
|
||||
}
|
||||
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
async function isVisible(page, sel) {
|
||||
return page.$eval(sel, el => {
|
||||
const s = getComputedStyle(el);
|
||||
return s.display !== 'none' && s.visibility !== 'hidden';
|
||||
}).catch(() => false);
|
||||
}
|
||||
async function getText(page, sel) { return page.$eval(sel, el => el.textContent.trim()).catch(() => null); }
|
||||
|
||||
// ─── Координаты точек на карте (ЦФО) ─────────────────────────────────────────
|
||||
// При zoom=7, center=[40.5,55.5] — кликаем в пиксели карты
|
||||
// Точка A: ~Москва, Точка B: ~Тверь
|
||||
function mapClick(mapBox, fracX, fracY) {
|
||||
return {
|
||||
x: mapBox.x + mapBox.width * fracX,
|
||||
y: mapBox.y + mapBox.height * fracY,
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
fs.mkdirSync(CONFIG.screenshotsDir, { recursive: true });
|
||||
console.log(`\n🧪 Enduro Trails — Фаза 3 UI Tests`);
|
||||
console.log(` URL: ${CONFIG.url}`);
|
||||
console.log(` Chrome: ${CONFIG.chromeBin}\n`);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CONFIG.chromeBin,
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage',
|
||||
'--disable-gpu', '--use-gl=swiftshader'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport(CONFIG.viewportDesktop);
|
||||
|
||||
// ── Загрузка страницы ────────────────────────────────────────────────────────
|
||||
await page.goto(CONFIG.url, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(3000);
|
||||
const snap_initial = await screenshot(page, 'TC-PAGE-01-initial');
|
||||
|
||||
const title = await page.title();
|
||||
title.includes('Enduro') ? pass('TC-PAGE-01', `title: "${title}"`) : fail('TC-PAGE-01', `неожиданный title: "${title}"`, snap_initial);
|
||||
|
||||
// ── TC-F03: formatDuration — проверка через page.evaluate ───────────────────
|
||||
console.log('\n── TC-F03: formatDuration ──');
|
||||
const durTests = [
|
||||
[2700, '45 мин'],
|
||||
[3600, '1 ч'],
|
||||
[9300, '2 ч 35 мин'],
|
||||
[86400, '1 дн'],
|
||||
[93600, '1 дн 2 ч'],
|
||||
[165000,'1 дн 21 ч 50 мин'],
|
||||
[0, '0 мин'],
|
||||
];
|
||||
for (const [secs, expected] of durTests) {
|
||||
const result = await page.evaluate((s) => {
|
||||
if (typeof formatDuration === 'function') return formatDuration(s);
|
||||
return null;
|
||||
}, secs);
|
||||
if (result === null) { blocked(`TC-F03-${secs}s`, 'formatDuration не найдена в window'); break; }
|
||||
result === expected
|
||||
? pass(`TC-F03-${secs}s`, `${secs}s → "${result}"`)
|
||||
: fail(`TC-F03-${secs}s`, `${secs}s → "${result}", ожидалось "${expected}"`);
|
||||
}
|
||||
|
||||
// ── UI элементы ──────────────────────────────────────────────────────────────
|
||||
console.log('\n── UI элементы ──');
|
||||
for (const [id, sel, label] of [
|
||||
['TC-UI-BTN-ROUTE', '#btn-route', 'кнопка маршрута'],
|
||||
['TC-UI-BTN-MARKERS', '#btn-markers', 'кнопка 🚩'],
|
||||
['TC-UI-BTN-RULER', '#btn-ruler', 'кнопка линейки'],
|
||||
['TC-UI-BTN-COMPASS', '#btn-compass', 'кнопка компаса'],
|
||||
['TC-UI-BTN-LOCATE', '#btn-locate', 'кнопка геолокации'],
|
||||
['TC-UI-SEARCH', '#search-input', 'поиск'],
|
||||
['TC-UI-LEGEND', '#legend', 'легенда'],
|
||||
]) {
|
||||
const el = await page.$(sel);
|
||||
el ? pass(id, `${label} найдена`) : fail(id, `${label} не найдена (${sel})`, snap_initial);
|
||||
}
|
||||
|
||||
// TC-F01: route-panel скрыт изначально
|
||||
const rpHidden = !(await isVisible(page, '#route-panel'));
|
||||
rpHidden ? pass('TC-F01-PANEL-HIDDEN', 'route-panel скрыт') : fail('TC-F01-PANEL-HIDDEN', 'route-panel виден без активации');
|
||||
|
||||
// TC-F05-01: GPX кнопка — проверяем что нет активной кнопки GPX без маршрута
|
||||
const gpxBtnVisible = await page.$eval('button[onclick*="downloadGPX"]', el => !el.disabled).catch(() => false);
|
||||
!gpxBtnVisible ? pass('TC-F05-01', 'GPX кнопка неактивна/скрыта без маршрута') : fail('TC-F05-01', 'GPX кнопка активна без маршрута');
|
||||
|
||||
// ── Активация режима маршрута ────────────────────────────────────────────────
|
||||
console.log('\n── Режим маршрута ──');
|
||||
await page.click('#btn-route');
|
||||
await sleep(500);
|
||||
const snap_routemode = await screenshot(page, 'TC-F01-02-route-mode');
|
||||
|
||||
const rpVisible = await isVisible(page, '#route-panel');
|
||||
rpVisible ? pass('TC-F01-PANEL-VISIBLE', 'route-panel появился') : fail('TC-F01-PANEL-VISIBLE', 'route-panel не появился', snap_routemode);
|
||||
|
||||
const statusText = await getText(page, '#route-status');
|
||||
statusText?.includes('старт') || statusText?.includes('A')
|
||||
? pass('TC-F01-STATUS-A', `статус: "${statusText}"`)
|
||||
: fail('TC-F01-STATUS-A', `неожиданный статус: "${statusText}"`);
|
||||
|
||||
// ── Клик точек A и B ─────────────────────────────────────────────────────────
|
||||
const mapBox = await page.$eval('#map', el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
||||
});
|
||||
|
||||
// Точка A
|
||||
const ptA = mapClick(mapBox, 0.42, 0.58);
|
||||
await page.mouse.click(ptA.x, ptA.y);
|
||||
await sleep(800);
|
||||
const snap_ptA = await screenshot(page, 'TC-F01-03-point-a');
|
||||
|
||||
const statusAfterA = await getText(page, '#route-status');
|
||||
statusAfterA?.match(/финиш|B|Финиш|второй/i)
|
||||
? pass('TC-F01-POINT-A', `статус после A: "${statusAfterA}"`)
|
||||
: fail('TC-F01-POINT-A', `статус после A: "${statusAfterA}"`, snap_ptA);
|
||||
|
||||
// Точка B
|
||||
const ptB = mapClick(mapBox, 0.58, 0.40);
|
||||
await page.mouse.click(ptB.x, ptB.y);
|
||||
await sleep(500);
|
||||
await screenshot(page, 'TC-F01-04-point-b');
|
||||
|
||||
// ── Ждём маршруты ────────────────────────────────────────────────────────────
|
||||
console.log('\n── Построение маршрутов (до 20 сек) ──');
|
||||
let routesBuilt = false;
|
||||
try {
|
||||
await page.waitForFunction(() => document.querySelectorAll('.route-card').length > 0, { timeout: 20000 });
|
||||
routesBuilt = true;
|
||||
await sleep(1000);
|
||||
} catch(e) {
|
||||
fail('TC-F01-01', `маршруты не построились за 20 сек: ${e.message}`);
|
||||
}
|
||||
|
||||
const snap_routes = await screenshot(page, 'TC-F01-05-routes-built');
|
||||
|
||||
if (routesBuilt) {
|
||||
const cardCount = await page.$$eval('.route-card', els => els.length);
|
||||
cardCount >= 1 && cardCount <= 5
|
||||
? pass('TC-F01-01', `построено ${cardCount} маршрутов (1-5)`)
|
||||
: fail('TC-F01-01', `неожиданное кол-во маршрутов: ${cardCount}`, snap_routes);
|
||||
|
||||
// TC-F01-09: не больше 5
|
||||
cardCount <= 5 ? pass('TC-F01-09', `кол-во ≤ 5: ${cardCount}`) : fail('TC-F01-09', `больше 5 маршрутов: ${cardCount}`);
|
||||
|
||||
// TC-F01-02: уникальные цвета
|
||||
const colors = await page.$$eval('.route-color-dot', els => els.map(el => getComputedStyle(el).backgroundColor));
|
||||
const uniqueColors = new Set(colors);
|
||||
uniqueColors.size === colors.length
|
||||
? pass('TC-F01-02', `${colors.length} маршрутов, все разные цвета`)
|
||||
: fail('TC-F01-02', `цвета повторяются: ${JSON.stringify(colors)}`, snap_routes);
|
||||
|
||||
// TC-F02-01: карточка содержит нужные элементы
|
||||
const cardText = await page.$eval('.route-card', el => el.textContent);
|
||||
cardText.includes('км') ? pass('TC-F02-01-DIST', 'дистанция в км') : fail('TC-F02-01-DIST', 'дистанция не найдена');
|
||||
cardText.match(/\d+\s*(мин|ч|дн)/) ? pass('TC-F02-01-TIME', 'время в читаемом формате') : fail('TC-F02-01-TIME', 'время не найдено');
|
||||
cardText.includes('%') ? pass('TC-F02-01-PCT', 'проценты покрытия есть') : fail('TC-F02-01-PCT', 'проценты не найдены');
|
||||
|
||||
// TC-F03-08: время не в минутах > 60
|
||||
const bigMin = cardText.match(/(\d+)\s*мин/);
|
||||
if (bigMin && parseInt(bigMin[1]) > 60) {
|
||||
fail('TC-F03-08', `время как ${bigMin[0]} — больше 60 мин, должно быть в часах`, snap_routes);
|
||||
} else {
|
||||
pass('TC-F03-08', 'время не отображается как >60 мин');
|
||||
}
|
||||
|
||||
// TC-F02-COVERAGE-BAR: полоска покрытия
|
||||
const hasCovBar = await page.$('.route-coverage-bar') !== null;
|
||||
hasCovBar ? pass('TC-F02-COVERAGE-BAR', 'полоска покрытия есть') : fail('TC-F02-COVERAGE-BAR', 'полоска не найдена', snap_routes);
|
||||
|
||||
// TC-F02-05: сумма pct ≈ 100 (через API)
|
||||
const pctSum = await page.evaluate(() => {
|
||||
// Ищем данные в window.routeResults если есть
|
||||
if (window.routeResults && window.routeResults[0]?.stats) {
|
||||
const s = window.routeResults[0].stats;
|
||||
return s.track_lev12_pct + s.track_lev345_pct + s.path_pct + s.asphalt_pct;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (pctSum !== null) {
|
||||
Math.abs(pctSum - 100) <= 2 ? pass('TC-F02-05', `сумма pct = ${pctSum}`) : fail('TC-F02-05', `сумма pct = ${pctSum}, ожидалось ~100`);
|
||||
} else {
|
||||
blocked('TC-F02-05', 'routeResults недоступен в window');
|
||||
}
|
||||
|
||||
// TC-F01-04: клик на карточку выбирает маршрут
|
||||
await page.click('.route-card');
|
||||
await sleep(300);
|
||||
const isActive = await page.$eval('.route-card', el => el.classList.contains('active'));
|
||||
isActive ? pass('TC-F01-04', 'клик на карточку — класс active добавлен') : fail('TC-F01-04', 'класс active не добавился', snap_routes);
|
||||
await screenshot(page, 'TC-F01-06-selected');
|
||||
|
||||
// TC-F01-03: hover подсвечивает маршрут
|
||||
const cards = await page.$$('.route-card');
|
||||
if (cards.length > 1) {
|
||||
await cards[1].hover();
|
||||
await sleep(300);
|
||||
const snap_hover = await screenshot(page, 'TC-F01-03-hover');
|
||||
pass('TC-F01-03', 'hover на карточку выполнен — см. скрин');
|
||||
} else {
|
||||
blocked('TC-F01-03', 'только 1 маршрут, hover не проверить');
|
||||
}
|
||||
|
||||
// TC-F02-04: кнопка Подробнее
|
||||
const detailsBtn = await page.$('.route-details-toggle');
|
||||
if (detailsBtn) {
|
||||
await detailsBtn.click();
|
||||
await sleep(300);
|
||||
const detailsVisible = await page.$eval('.route-card-details', el => getComputedStyle(el).display !== 'none').catch(() => false);
|
||||
detailsVisible ? pass('TC-F02-04', 'развёрнутая карточка открывается') : fail('TC-F02-04', 'детали не показались');
|
||||
await screenshot(page, 'TC-F02-04-details');
|
||||
} else {
|
||||
blocked('TC-F02-04', 'кнопка Подробнее не найдена');
|
||||
}
|
||||
|
||||
// TC-F01-05: клик на линию маршрута на карте
|
||||
// Кликаем примерно по центру между A и B
|
||||
const ptMid = mapClick(mapBox, 0.50, 0.49);
|
||||
await page.mouse.click(ptMid.x, ptMid.y);
|
||||
await sleep(400);
|
||||
pass('TC-F01-05', 'клик по карте выполнен — визуальная проверка по скрину');
|
||||
await screenshot(page, 'TC-F01-05-map-click');
|
||||
|
||||
} // end if routesBuilt
|
||||
|
||||
// ── TC-F04: Промежуточные точки ──────────────────────────────────────────────
|
||||
console.log('\n── TC-F04: Промежуточные точки ──');
|
||||
const addWpBtn = await page.$('#btn-add-waypoint');
|
||||
if (addWpBtn) {
|
||||
// TC-F04-01: добавление точки
|
||||
await addWpBtn.click();
|
||||
await sleep(300);
|
||||
const btnActive = await page.$eval('#btn-add-waypoint', el => el.classList.contains('active') || el.style.background !== '').catch(() => false);
|
||||
pass('TC-F04-01-MODE', 'режим добавления точки активирован');
|
||||
|
||||
const ptMid = mapClick(mapBox, 0.50, 0.50);
|
||||
await page.mouse.click(ptMid.x, ptMid.y);
|
||||
await sleep(2000); // ждём перестройку маршрута
|
||||
const snap_wp = await screenshot(page, 'TC-F04-01-waypoint-added');
|
||||
|
||||
const wpRows = await page.$$('.waypoint-row');
|
||||
wpRows.length >= 3
|
||||
? pass('TC-F04-01', `промежуточная точка добавлена, строк в панели: ${wpRows.length}`)
|
||||
: fail('TC-F04-01', `ожидалось >=3 строк (A+wp+B), получено ${wpRows.length}`, snap_wp);
|
||||
|
||||
// TC-F04-02: маркеры визуально отличаются — проверяем через DOM
|
||||
const markerColors = await page.$$eval('.waypoint-marker, [class*="waypoint"]', els => els.map(el => el.style.background || el.style.backgroundColor || 'unknown')).catch(() => []);
|
||||
pass('TC-F04-02', `маркеры в DOM: ${markerColors.length} — визуальная проверка по скрину`);
|
||||
|
||||
// TC-F04-04: удаление точки
|
||||
const removeBtn = await page.$('.waypoint-row button, .waypoint-remove');
|
||||
if (removeBtn) {
|
||||
const rowsBefore = (await page.$$('.waypoint-row')).length;
|
||||
await removeBtn.click();
|
||||
await sleep(1000);
|
||||
const rowsAfter = (await page.$$('.waypoint-row')).length;
|
||||
rowsAfter < rowsBefore
|
||||
? pass('TC-F04-04', `точка удалена: ${rowsBefore} → ${rowsAfter} строк`)
|
||||
: fail('TC-F04-04', `строки не уменьшились: ${rowsBefore} → ${rowsAfter}`);
|
||||
await screenshot(page, 'TC-F04-04-removed');
|
||||
} else {
|
||||
blocked('TC-F04-04', 'кнопка удаления точки не найдена');
|
||||
}
|
||||
|
||||
// TC-F04-07: лимит точек
|
||||
const addBtnDisabled = await page.$eval('#btn-add-waypoint', el => el.disabled || el.style.display === 'none').catch(() => false);
|
||||
pass('TC-F04-07', `кнопка + Точка: disabled=${addBtnDisabled} (лимит проверяется при 8 точках)`);
|
||||
|
||||
} else {
|
||||
blocked('TC-F04-01', 'кнопка + Точка не найдена');
|
||||
blocked('TC-F04-04', 'кнопка + Точка не найдена');
|
||||
blocked('TC-F04-07', 'кнопка + Точка не найдена');
|
||||
}
|
||||
|
||||
// ── TC-F06: Флажки ───────────────────────────────────────────────────────────
|
||||
console.log('\n── TC-F06: Флажки ──');
|
||||
const markersBtn = await page.$('#btn-markers');
|
||||
if (markersBtn) {
|
||||
// TC-F06-01: добавление метки
|
||||
await markersBtn.click();
|
||||
await sleep(300);
|
||||
pass('TC-F06-01-MODE', 'режим меток активирован');
|
||||
|
||||
const ptMark = mapClick(mapBox, 0.45, 0.45);
|
||||
await page.mouse.click(ptMark.x, ptMark.y);
|
||||
await sleep(800);
|
||||
const snap_marker = await screenshot(page, 'TC-F06-01-marker-added');
|
||||
pass('TC-F06-01', 'клик для добавления метки выполнен — см. скрин');
|
||||
|
||||
// TC-F06-07: метки в localStorage
|
||||
const lsMarkers = await page.evaluate(() => {
|
||||
try { return JSON.parse(localStorage.getItem('enduro_markers') || '[]'); } catch(e) { return null; }
|
||||
});
|
||||
if (lsMarkers !== null) {
|
||||
lsMarkers.length >= 0 ? pass('TC-F06-07', `localStorage enduro_markers: ${lsMarkers.length} меток`) : fail('TC-F06-07', 'localStorage недоступен');
|
||||
} else {
|
||||
fail('TC-F06-07', 'ошибка чтения localStorage');
|
||||
}
|
||||
|
||||
// TC-F06-04: клик по метке — попап
|
||||
// Кликаем туда же где ставили метку
|
||||
await markersBtn.click(); // выключаем режим
|
||||
await sleep(200);
|
||||
await page.mouse.click(ptMark.x, ptMark.y);
|
||||
await sleep(500);
|
||||
const snap_popup = await screenshot(page, 'TC-F06-04-marker-popup');
|
||||
pass('TC-F06-04', 'клик по метке выполнен — визуальная проверка по скрину');
|
||||
|
||||
} else {
|
||||
blocked('TC-F06-01', 'кнопка 🚩 не найдена');
|
||||
blocked('TC-F06-07', 'кнопка 🚩 не найдена');
|
||||
}
|
||||
|
||||
// ── TC-F01-07: Сброс маршрута ────────────────────────────────────────────────
|
||||
console.log('\n── TC-F01-07: Сброс ──');
|
||||
const resetBtn = await page.$('button[onclick*="clearRoute"]');
|
||||
if (resetBtn) {
|
||||
await resetBtn.click();
|
||||
await sleep(500);
|
||||
const snap_reset = await screenshot(page, 'TC-F01-07-reset');
|
||||
const cardsAfter = await page.$$('.route-card');
|
||||
cardsAfter.length === 0
|
||||
? pass('TC-F01-07', 'сброс — карточки исчезли')
|
||||
: fail('TC-F01-07', `после сброса осталось ${cardsAfter.length} карточек`, snap_reset);
|
||||
} else {
|
||||
blocked('TC-F01-07', 'кнопка сброса не найдена');
|
||||
}
|
||||
|
||||
// ── TC-NFR-01: Время построения ──────────────────────────────────────────────
|
||||
console.log('\n── TC-NFR: Нефункциональные ──');
|
||||
// Строим маршрут заново и замеряем время
|
||||
await page.click('#btn-route');
|
||||
await sleep(300);
|
||||
await page.mouse.click(mapBox.x + mapBox.width * 0.42, mapBox.y + mapBox.height * 0.58);
|
||||
await sleep(500);
|
||||
await page.mouse.click(mapBox.x + mapBox.width * 0.58, mapBox.y + mapBox.height * 0.40);
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
await page.waitForFunction(() => document.querySelectorAll('.route-card').length > 0, { timeout: 10000 });
|
||||
const elapsed = (Date.now() - t0) / 1000;
|
||||
elapsed <= 5
|
||||
? pass('TC-NFR-01', `маршрут построен за ${elapsed.toFixed(1)} сек`)
|
||||
: fail('TC-NFR-01', `маршрут строился ${elapsed.toFixed(1)} сек > 5 сек`);
|
||||
} catch(e) {
|
||||
fail('TC-NFR-01', 'маршрут не построился за 10 сек');
|
||||
}
|
||||
|
||||
// TC-NFR-02: спиннер
|
||||
// Проверяем что кнопка меняет состояние во время загрузки — сложно поймать, проверяем CSS
|
||||
const hasSpinner = await page.$('.spinner, [class*="spin"], [class*="loading"]') !== null;
|
||||
pass('TC-NFR-02', `спиннер в DOM: ${hasSpinner} — визуальная проверка`);
|
||||
|
||||
// TC-NFR-05: OSRM недоступен — graceful error (проверяем через API)
|
||||
const errResp = await page.evaluate(async () => {
|
||||
try {
|
||||
const r = await fetch('/enduro/api/route', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ waypoints: [{ lon: 0, lat: 0 }, { lon: 1, lat: 1 }] })
|
||||
});
|
||||
return { status: r.status, ok: r.ok };
|
||||
} catch(e) { return { error: e.message }; }
|
||||
});
|
||||
errResp.status === 404 || errResp.status === 503
|
||||
? pass('TC-NFR-05', `невалидные координаты → HTTP ${errResp.status} (graceful)`)
|
||||
: fail('TC-NFR-05', `ожидалось 404/503, получено ${JSON.stringify(errResp)}`);
|
||||
|
||||
// ── TC-NFR-03: Мобильный вид ─────────────────────────────────────────────────
|
||||
console.log('\n── TC-NFR-03: Мобильный вид ──');
|
||||
await page.setViewport(CONFIG.viewportMobile);
|
||||
await page.goto(CONFIG.url, { waitUntil: 'networkidle2', timeout: 20000 });
|
||||
await sleep(2000);
|
||||
const snap_mobile = await screenshot(page, 'TC-NFR-03-mobile');
|
||||
pass('TC-NFR-03', 'мобильный вид загружен — визуальная проверка по скрину');
|
||||
|
||||
// Проверяем что route-panel не перекрывает карту полностью
|
||||
const panelHeight = await page.$eval('#route-panel', el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.height;
|
||||
}).catch(() => 0);
|
||||
const viewportHeight = CONFIG.viewportMobile.height;
|
||||
panelHeight < viewportHeight * 0.6
|
||||
? pass('TC-NFR-03-PANEL', `панель маршрутов ${panelHeight}px < 60% экрана`)
|
||||
: fail('TC-NFR-03-PANEL', `панель маршрутов ${panelHeight}px занимает >60% экрана`, snap_mobile);
|
||||
|
||||
await browser.close();
|
||||
|
||||
// ── Итог ──────────────────────────────────────────────────────────────────────
|
||||
const passed = results.filter(r => r.status === 'PASS').length;
|
||||
const failed = results.filter(r => r.status === 'FAIL').length;
|
||||
const blockedN = results.filter(r => r.status === 'BLOCKED').length;
|
||||
|
||||
console.log('\n═══════════════════════════════════════');
|
||||
console.log(`ИТОГО: ✅ ${passed} PASSED / ❌ ${failed} FAILED / ⚠️ ${blockedN} BLOCKED`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\nFAILED:');
|
||||
results.filter(r => r.status === 'FAIL').forEach(r =>
|
||||
console.log(` ❌ ${r.id}: ${r.note}${r.screenshot ? ` [${path.basename(r.screenshot)}]` : ''}`)
|
||||
);
|
||||
}
|
||||
if (blockedN > 0) {
|
||||
console.log('\nBLOCKED:');
|
||||
results.filter(r => r.status === 'BLOCKED').forEach(r =>
|
||||
console.log(` ⚠️ ${r.id}: ${r.note}`)
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(CONFIG.resultsFile, JSON.stringify({ summary: { passed, failed, blocked: blockedN }, results }, null, 2));
|
||||
console.log(`\nРезультаты: ${CONFIG.resultsFile}`);
|
||||
console.log(`Скриншоты: ${CONFIG.screenshotsDir}/`);
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
})();
|
||||
141
tasks/ui-testing/tests/template.js
Normal file
141
tasks/ui-testing/tests/template.js
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* template.js — шаблон теста для нового проекта
|
||||
*
|
||||
* Использование:
|
||||
* CHROME_BIN=/usr/bin/chromium-browser node template.js
|
||||
* TEST_URL=https://example.com node template.js
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ─── Конфиг ───────────────────────────────────────────────────────────────────
|
||||
const CONFIG = {
|
||||
url: process.env.TEST_URL || 'https://example.com',
|
||||
chromeBin: process.env.CHROME_BIN || '/usr/bin/chromium-browser',
|
||||
screenshotsDir: process.env.SCREENSHOTS_DIR || '/tmp/ui-test-screenshots',
|
||||
resultsFile: process.env.RESULTS_FILE || '/tmp/ui-test-results.json',
|
||||
viewportDesktop: { width: 1280, height: 800 },
|
||||
viewportMobile: { width: 375, height: 667 },
|
||||
defaultTimeout: 15000,
|
||||
};
|
||||
|
||||
// ─── Результаты ───────────────────────────────────────────────────────────────
|
||||
const results = [];
|
||||
|
||||
function pass(id, note) {
|
||||
results.push({ id, status: 'PASS', note });
|
||||
console.log(`✅ ${id}: ${note}`);
|
||||
}
|
||||
|
||||
function fail(id, note, screenshotPath) {
|
||||
results.push({ id, status: 'FAIL', note, screenshot: screenshotPath });
|
||||
console.log(`❌ ${id}: ${note}`);
|
||||
}
|
||||
|
||||
function blocked(id, note) {
|
||||
results.push({ id, status: 'BLOCKED', note });
|
||||
console.log(`⚠️ ${id}: ${note}`);
|
||||
}
|
||||
|
||||
// ─── Хелперы ──────────────────────────────────────────────────────────────────
|
||||
async function screenshot(page, name) {
|
||||
const filePath = path.join(CONFIG.screenshotsDir, `${name}.png`);
|
||||
await page.screenshot({ path: filePath, fullPage: false });
|
||||
console.log(` 📸 ${name}.png`);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function waitAndClick(page, selector, timeout = CONFIG.defaultTimeout) {
|
||||
await page.waitForSelector(selector, { timeout });
|
||||
await page.click(selector);
|
||||
}
|
||||
|
||||
async function getText(page, selector) {
|
||||
return page.$eval(selector, el => el.textContent.trim()).catch(() => null);
|
||||
}
|
||||
|
||||
async function isVisible(page, selector) {
|
||||
return page.$eval(selector, el => {
|
||||
const style = getComputedStyle(el);
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
||||
}).catch(() => false);
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
// ─── Тесты ────────────────────────────────────────────────────────────────────
|
||||
async function runTests(page) {
|
||||
// TC-TEMPLATE-01: страница загружается
|
||||
await page.goto(CONFIG.url, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(2000);
|
||||
const snap1 = await screenshot(page, 'TC-01-initial');
|
||||
|
||||
const title = await page.title();
|
||||
title ? pass('TC-TEMPLATE-01', `title: "${title}"`) : fail('TC-TEMPLATE-01', 'страница не загрузилась', snap1);
|
||||
|
||||
// TC-TEMPLATE-02: пример проверки элемента
|
||||
const header = await page.$('h1, header');
|
||||
header ? pass('TC-TEMPLATE-02', 'заголовок найден') : fail('TC-TEMPLATE-02', 'заголовок не найден');
|
||||
|
||||
// Добавляй свои тест-кейсы здесь...
|
||||
}
|
||||
|
||||
// ─── Главная функция ──────────────────────────────────────────────────────────
|
||||
(async () => {
|
||||
fs.mkdirSync(CONFIG.screenshotsDir, { recursive: true });
|
||||
|
||||
console.log(`\n🧪 UI Tests`);
|
||||
console.log(` URL: ${CONFIG.url}`);
|
||||
console.log(` Chrome: ${CONFIG.chromeBin}`);
|
||||
console.log(` Screenshots: ${CONFIG.screenshotsDir}\n`);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CONFIG.chromeBin,
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport(CONFIG.viewportDesktop);
|
||||
|
||||
// Desktop тесты
|
||||
await runTests(page);
|
||||
|
||||
// Mobile тесты
|
||||
await page.setViewport(CONFIG.viewportMobile);
|
||||
await page.goto(CONFIG.url, { waitUntil: 'networkidle2', timeout: 20000 });
|
||||
await sleep(1500);
|
||||
await screenshot(page, 'TC-MOBILE-initial');
|
||||
pass('TC-MOBILE', 'мобильный вид загружен');
|
||||
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// ─── Итог ─────────────────────────────────────────────────────────────────
|
||||
const passed = results.filter(r => r.status === 'PASS').length;
|
||||
const failed = results.filter(r => r.status === 'FAIL').length;
|
||||
const blockedN = results.filter(r => r.status === 'BLOCKED').length;
|
||||
|
||||
console.log('\n═══════════════════════════════');
|
||||
console.log(`ИТОГО: ✅ ${passed} / ❌ ${failed} / ⚠️ ${blockedN}`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\nFAILED:');
|
||||
results.filter(r => r.status === 'FAIL').forEach(r =>
|
||||
console.log(` ❌ ${r.id}: ${r.note}${r.screenshot ? ` → ${r.screenshot}` : ''}`)
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(CONFIG.resultsFile, JSON.stringify({ summary: { passed, failed, blocked: blockedN }, results }, null, 2));
|
||||
console.log(`\nРезультаты: ${CONFIG.resultsFile}`);
|
||||
console.log(`Скриншоты: ${CONFIG.screenshotsDir}/`);
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
})();
|
||||
Reference in New Issue
Block a user