auto-sync: 2026-05-04 12:20:01

This commit is contained in:
Stream
2026-05-04 12:20:17 +03:00
parent 73f213b7a4
commit 4836421910
6 changed files with 1012 additions and 0 deletions

View 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);
})();

View 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);
})();