452 lines
24 KiB
JavaScript
452 lines
24 KiB
JavaScript
#!/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);
|
||
})();
|