From 2573d81cff8dbe60483a0b10bba39f41e27983e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8NW=C2=A8?= <¨neroworld@mail.ru¨> Date: Fri, 17 Apr 2026 20:21:29 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20remove=20CBS-specific=20e2e-booking?= =?UTF-8?q?=20flow=20=E2=80=94=20belongs=20to=20CBS=20project,=20not=20APA?= =?UTF-8?q?W=20starter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kilo/agents/visual-tester.md | 7 +- .kilo/commands/e2e-test.md | 11 +- .kilo/commands/web-test.md | 16 +- docker/docker-compose.web-testing.yml | 20 - tests/scripts/e2e-booking-flow-v2.js | 512 -------------------------- 5 files changed, 7 insertions(+), 559 deletions(-) delete mode 100644 tests/scripts/e2e-booking-flow-v2.js diff --git a/.kilo/agents/visual-tester.md b/.kilo/agents/visual-tester.md index aa20195..3aed395 100755 --- a/.kilo/agents/visual-tester.md +++ b/.kilo/agents/visual-tester.md @@ -64,7 +64,7 @@ docker compose -f docker/docker-compose.web-testing.yml run --rm \ # Full pipeline — external site (host network for DNS) NETWORK_MODE=host docker compose -f docker/docker-compose.web-testing.yml run --rm \ - -e TARGET_URL=https://irina-vik.ru visual-tester + -e TARGET_URL=https://example.com visual-tester # Capture baselines docker compose -f docker/docker-compose.web-testing.yml run --rm \ @@ -74,9 +74,7 @@ docker compose -f docker/docker-compose.web-testing.yml run --rm \ docker compose -f docker/docker-compose.web-testing.yml run --rm \ -e TARGET_URL=https://example.com console-monitor -# E2E booking flow (external site, host network required) -NETWORK_MODE=host docker compose -f docker/docker-compose.web-testing.yml run --rm \ - -e GITEA_ISSUE=42 e2e-booking + ``` > **Note**: External sites require `NETWORK_MODE=host` because Chromium inside @@ -91,7 +89,6 @@ NETWORK_MODE=host docker compose -f docker/docker-compose.web-testing.yml run -- | Capture | `tests/scripts/capture-screenshots.js` | baseline/current screenshot capture | | Compare | `tests/scripts/compare-screenshots.js` | Pixelmatch PNG comparison | | Console monitor | `tests/scripts/console-error-monitor-standalone.js` | Standalone console/network error detection + Gitea | -| E2E booking | `tests/scripts/e2e-booking-flow-v2.js` | Full booking flow on irina-vik.ru + Gitea | | Browser launcher | `tests/scripts/lib/browser-launcher.js` | Shared Playwright launch config (DNS fix) | | Gitea client | `tests/scripts/lib/gitea-client.js` | API client for posting results + attachments | diff --git a/.kilo/commands/e2e-test.md b/.kilo/commands/e2e-test.md index 82269e4..05a6ac1 100644 --- a/.kilo/commands/e2e-test.md +++ b/.kilo/commands/e2e-test.md @@ -31,7 +31,7 @@ docker compose -f docker/docker-compose.web-testing.yml run --rm \ ```bash NETWORK_MODE=host DNS_RESOLUTION_ORDER=hostname-first \ docker compose -f docker/docker-compose.web-testing.yml run --rm \ - -e TARGET_URL=https://example.com -e GITEA_ISSUE=42 e2e-booking + -e TARGET_URL=https://example.com -e GITEA_ISSUE=42 visual-tester ``` ### Available Services @@ -43,7 +43,7 @@ docker compose -f docker/docker-compose.web-testing.yml run --rm \ | `screenshot-current` | playwright:v1.52.0-noble | Capture current screenshots | | `visual-compare` | node:20-alpine | Pixelmatch comparison only | | `console-monitor` | playwright:v1.52.0-noble | Console/network errors | -| `e2e-booking` | playwright:v1.52.0-noble | Full booking flow (irina-vik.ru) | + ### DNS Note @@ -59,7 +59,6 @@ flag is added automatically via `lib/browser-launcher.js`. | `tests/scripts/capture-screenshots.js` | baseline/current screenshot capture | | `tests/scripts/compare-screenshots.js` | Pixelmatch PNG comparison | | `tests/scripts/console-error-monitor-standalone.js` | Console/network errors + Gitea | -| `tests/scripts/e2e-booking-flow-v2.js` | Register → Book → Login → Cabinet | | `tests/scripts/lib/browser-launcher.js` | Shared Playwright launch (DNS fix, UA) | | `tests/scripts/lib/gitea-client.js` | Gitea API client (comments, attachments) | @@ -84,12 +83,6 @@ Use Task tool with subagent_type: "visual-tester" prompt: "Test login flow at {url} with credentials from env, post results to Gitea Issue #{issue}" ``` -### E2E Booking Flow - -```bash -NETWORK_MODE=host GITEA_ISSUE=42 \ -docker compose -f docker/docker-compose.web-testing.yml run --rm e2e-booking -``` ## Gitea Integration diff --git a/.kilo/commands/web-test.md b/.kilo/commands/web-test.md index 8c31202..f788bb4 100644 --- a/.kilo/commands/web-test.md +++ b/.kilo/commands/web-test.md @@ -90,7 +90,7 @@ Run visual regression testing pipeline in Docker. Captures screenshots, extracts | `screenshot-current` | Capture current only | | `visual-compare` | pixelmatch comparison only | | `console-monitor` | Console/network errors only | -| `e2e-booking` | E2E booking flow (irina-vik.ru) | + ## Docker Networking @@ -106,20 +106,10 @@ docker compose -f docker/docker-compose.web-testing.yml up visual-tester ### External site testing (host network) -Required for testing external URLs (irina-vik.ru, etc.) where Docker DNS fails: +Required for testing external URLs where Docker DNS fails: ```bash -NETWORK_MODE=host docker compose -f docker/docker-compose.web-testing.yml up e2e-booking -``` - -Or per-run: - -```bash -docker run --rm --network host --shm-size=2g --ipc=host \ - -v ./tests:/app/tests \ - -e GITEA_ISSUE=42 \ - mcr.microsoft.com/playwright:v1.52.0-noble \ - sh -c "cd /app/tests && npm install --ignore-scripts 2>/dev/null && node scripts/e2e-booking-flow-v2.js" +NETWORK_MODE=host docker compose -f docker/docker-compose.web-testing.yml up visual-tester ``` The `NETWORK_MODE` env var controls `network_mode` in docker-compose. Default is `bridge` diff --git a/docker/docker-compose.web-testing.yml b/docker/docker-compose.web-testing.yml index 7901185..af079aa 100644 --- a/docker/docker-compose.web-testing.yml +++ b/docker/docker-compose.web-testing.yml @@ -127,23 +127,3 @@ services: ipc: host network_mode: ${NETWORK_MODE:-bridge} - # ─── E2E Booking Flow ────────────────────────────────────────── - e2e-booking: - image: mcr.microsoft.com/playwright:v1.52.0-noble - container_name: apaw-e2e-booking - working_dir: /app - volumes: - - ../tests:/app/tests - environment: - - TARGET_URL=${TARGET_URL:-https://irina-vik.ru} - - GITEA_ISSUE=${GITEA_ISSUE:-} - - GITEA_TOKEN=${GITEA_TOKEN:-} - - GITEA_USER=${GITEA_USER:-} - - GITEA_PASSWORD=${GITEA_PASSWORD:-} - - DNS_RESOLUTION_ORDER=hostname-first - command: > - sh -c "cd /app/tests && npm install --ignore-scripts 2>/dev/null; - node scripts/e2e-booking-flow-v2.js" - shm_size: '2gb' - ipc: host - network_mode: ${NETWORK_MODE:-host} \ No newline at end of file diff --git a/tests/scripts/e2e-booking-flow-v2.js b/tests/scripts/e2e-booking-flow-v2.js deleted file mode 100644 index 6f0d419..0000000 --- a/tests/scripts/e2e-booking-flow-v2.js +++ /dev/null @@ -1,512 +0,0 @@ -#!/usr/bin/env node -/** - * E2E Booking Flow — irina-vik.ru - * Register → Book service → Logout → Login → View appointments - * - * Environment: - * GITEA_ISSUE - Gitea issue to post results (optional) - */ -const fs = require('fs'); -const path = require('path'); -const gitea = require('./lib/gitea-client'); -const { launchBrowser, newContext, navigateTo, BASE_ARGS } = require('./lib/browser-launcher'); - -const BASE_URL = 'https://irina-vik.ru'; -const SCREENSHOT_DIR = path.join(__dirname, '..', 'visual', 'e2e'); -const GITEA_ISSUE = parseInt(process.env.GITEA_ISSUE, 10) || null; -const TIMESTAMP = Date.now(); - -const TEST_EMAIL = `apaw.test.${TIMESTAMP}@mailinator.com`; -const TEST_PASSWORD = 'TestPass123!'; -const TEST_NAME = 'Тест'; -const TEST_LASTNAME = 'Пользователев'; - -function ensureDir(d) { if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); } - -async function ss(page, name) { - const f = path.join(SCREENSHOT_DIR, `e2e-${name}.png`); - await page.screenshot({ path: f, fullPage: false }); - console.log(` 📸 ${name}`); - return f; -} - -async function main() { - console.log('═══════════════════════════════════════════════════'); - console.log(' E2E: Register → Book → Logout → Login → Cabinet'); - console.log('═══════════════════════════════════════════════════\n'); - ensureDir(SCREENSHOT_DIR); - - const steps = []; - const browser = await launchBrowser(); - const ctx = await newContext(browser); - const page = await ctx.newPage(); - - // ─── STEP 1: Register ───────────────────────────────────── - console.log('📍 Step 1: Registration'); - try { - await navigateTo(page, BASE_URL); - await page.waitForTimeout(3000); - await ss(page, '01-homepage'); - - // Open auth modal first (SPA requires clicking "Войти" in header) - const authModalOpened = await page.evaluate(() => { - const headerLinks = document.querySelectorAll('a, button'); - for (const l of headerLinks) { - const text = (l.textContent || '').trim(); - if (text === 'Войти' || text === 'ВОЙТИ') { - l.click(); - return text; - } - } - // Also try data attributes - const authBtn = document.querySelector('[data-bs-toggle="modal"], [data-toggle="modal"]'); - if (authBtn) { authBtn.click(); return 'modal-toggle'; } - return null; - }); - if (authModalOpened) { - console.log(` Opened auth modal: "${authModalOpened}"`); - await page.waitForTimeout(1500); - } - - // Open registration tab - await page.click('a[href="#tab-register"]').catch(() => {}); - // Fallback: try evaluating click in JS - await page.evaluate(() => { - const links = document.querySelectorAll('a'); - for (const a of links) { - if ((a.textContent || '').trim() === 'Регистрация' || (a.href || '').includes('#tab-register')) { - a.click(); - break; - } - } - }); - await page.waitForTimeout(1500); - await ss(page, '02-reg-tab'); - - // Wait for reg form to appear and scroll modal into view - await page.waitForSelector('#reg-name', { state: 'visible', timeout: 5000 }).catch(() => {}); - - // Force scroll modal into view - await page.evaluate(() => { - const modal = document.querySelector('.modal.show, .modal-dialog, #authModal'); - if (modal) modal.scrollIntoView(); - }); - await page.waitForTimeout(500); - - // Fill registration form - const regFields = [ - { selector: '#reg-name', value: TEST_NAME }, - { selector: '#reg-lastname', value: TEST_LASTNAME }, - { selector: '#reg-email', value: TEST_EMAIL }, - { selector: '#reg-phone', value: '+7 (999) 123-45-67' }, - { selector: '#reg-password', value: TEST_PASSWORD }, - ]; - - for (const f of regFields) { - try { - await page.fill(f.selector, f.value, { timeout: 3000 }); - console.log(` Filled: ${f.selector}`); - } catch (e) { - // Try clicking label first then filling - try { - await page.click(f.selector, { timeout: 2000 }); - await page.waitForTimeout(300); - await page.fill(f.selector, f.value, { timeout: 3000 }); - console.log(` Filled (after click): ${f.selector}`); - } catch (e2) { - console.log(` ⚠️ Could not fill: ${f.selector} - ${e2.message.slice(0, 60)}`); - } - } - } - - await ss(page, '03-reg-filled'); - - // Submit registration - const submitResult = await page.evaluate(() => { - const btns = document.querySelectorAll('#tab-register button, #tab-register [type="submit"], .modal button[type="submit"]'); - for (const b of btns) { - const text = (b.textContent || '').trim().toLowerCase(); - if (text.includes('зарегистриров') || text.includes('регистрац') || text.includes('создать')) { - b.click(); - return text; - } - } - // Fallback: find any submit in modal - const modal = document.querySelector('.modal.show'); - if (modal) { - const btn = modal.querySelector('button:not([class*="close"]):not([class*="back"])'); - if (btn) { btn.click(); return btn.textContent.trim().slice(0, 60); } - } - return null; - }); - - if (submitResult) { - console.log(` Clicked: "${submitResult}"`); - await page.waitForTimeout(3000); - await ss(page, '04-reg-result'); - const resultText = await page.evaluate(() => document.body?.innerText?.slice(0, 1000) || ''); - if (resultText.includes('подтверд') || resultText.includes('успешн') || resultText.includes('письм') || resultText.includes('отправлен')) { - console.log(' ✅ Registration successful'); - steps.push({ step: 'register', status: 'PASS' }); - } else if (resultText.includes('уже существ') || resultText.includes('занят') || resultText.includes('зарегистрирован')) { - console.log(' ⚠️ Email already registered'); - steps.push({ step: 'register', status: 'PARTIAL', note: 'email exists' }); - } else { - console.log(` Result text: ${resultText.slice(0, 200)}`); - steps.push({ step: 'register', status: 'DONE', note: 'form submitted' }); - } - } else { - console.log(' ⚠️ No submit button found'); - steps.push({ step: 'register', status: 'PARTIAL', note: 'form filled, no submit' }); - } - } catch (err) { - console.log(` ❌ ${err.message.slice(0, 120)}`); - steps.push({ step: 'register', status: 'FAIL', error: err.message.slice(0, 80) }); - } - - // ─── STEP 2: Book a service ─────────────────────────────── - console.log('\n📍 Step 2: Book a service'); - try { - // Reload to reset state - await navigateTo(page, BASE_URL); - await page.waitForTimeout(3000); - - // Scroll to booking section and click service - await page.evaluate(() => { - document.getElementById('page-booking')?.scrollIntoView(); - // Click first service option - const svc = document.querySelector('.service-option'); - if (svc) svc.click(); - return true; - }); - await page.waitForTimeout(2000); - - // Now call selectSvcOpt to properly advance to step 2 - const serviceSelected = await page.evaluate(() => { - if (typeof selectSvcOpt === 'function') { - const opts = document.querySelectorAll('.service-option'); - if (opts.length > 0) { - const opt = opts[0]; - const price = opt.getAttribute('data-price') || '3500'; - const id = opt.getAttribute('data-id') || '10'; - selectSvcOpt(opt, parseInt(id), opt.querySelector('div > div')?.textContent?.trim() || 'Консультация', parseInt(price), 60); - return { clicked: opt.textContent.trim().slice(0, 80) }; - } - } - // Fallback: just click - const first = document.querySelector('.service-option'); - if (first) { first.click(); return { clicked: first.textContent.trim().slice(0, 80) }; } - return null; - }); - console.log(` Service selected: ${JSON.stringify(serviceSelected)}`); - await page.waitForTimeout(2000); - await ss(page, '05-booking-step1-service'); - - // Step 2: Pick a date from the calendar - const datePicked = await page.evaluate(() => { - // Find available days in calendar - const available = document.querySelectorAll('.calendar-day.available, .day.available, td.available, [class*="day"][class*="available"]'); - if (available.length > 0) { - available[0].click(); - return { clicked: available[0].textContent.trim(), count: available.length }; - } - // Try calendar-day - const calDays = document.querySelectorAll('[class*="calendar"] [class*="day"]:not(.disabled):not(.past):not(.empty)'); - for (const d of calDays) { - const text = d.textContent.trim(); - if (text && parseInt(text) > 0) { - d.click(); - return { clicked: text, count: calDays.length }; - } - } - return null; - }); - console.log(` Date picked: ${JSON.stringify(datePicked)}`); - await page.waitForTimeout(1500); - await ss(page, '06-booking-step2-date'); - - if (datePicked) { - // Step 3: Pick a time - const timePicked = await page.evaluate(() => { - const slots = document.querySelectorAll('[class*="time-slot"], [class*="slot"]:not(.disabled), .time-option, [class*="slot"][class*="available"]'); - if (slots.length > 0) { - slots[0].click(); - return { clicked: slots[0].textContent.trim(), count: slots.length }; - } - // Try buttons with time - const btns = document.querySelectorAll('.booking-body button:not(.btn-booking-back):not(.calendar-nav)'); - for (const b of btns) { - const text = b.textContent.trim(); - if (text.match(/\d{1,2}:\d{2}/)) { - b.click(); - return { clicked: text, count: btns.length }; - } - } - return null; - }); - console.log(` Time picked: ${JSON.stringify(timePicked)}`); - await page.waitForTimeout(1000); - await ss(page, '07-booking-step3-time'); - - // Step 4: Fill personal data and submit - const dataFilled = await page.evaluate(() => { - const nameInput = document.querySelector('#b-name, [name*="name"], .booking-body input[placeholder*="Имя"]'); - const emailInput = document.querySelector('#b-email, [name*="email"], .booking-body input[placeholder*="email"]'); - const phoneInput = document.querySelector('#b-phone, [name*="phone"], .booking-body input[placeholder*="телефон"]'); - return { - hasName: !!nameInput, - hasEmail: !!emailInput, - hasPhone: !!phoneInput, - }; - }); - console.log(` Data form fields: ${JSON.stringify(dataFilled)}`); - - if (dataFilled.hasName || dataFilled.hasEmail) { - // Since we already registered/logged in, this might be auto-filled or have "login" button - const submitted = await page.evaluate(() => { - // Look for submit/confirm button - const btns = document.querySelectorAll('.booking-body .btn-booking-next, .booking-body button[type="submit"], .booking-body button'); - for (const b of btns) { - const text = b.textContent.trim().toLowerCase(); - if (text.includes('отправ') || text.includes('подтверд') || text.includes('записать') || text.includes('далее') || text.includes('войти')) { - b.click(); - return text.slice(0, 60); - } - } - return null; - }); - console.log(` Clicked: "${submitted}"`); - await page.waitForTimeout(3000); - } - await ss(page, '08-booking-result'); - steps.push({ step: 'booking', status: 'PASS', service: serviceSelected?.clicked, date: datePicked?.clicked }); - } else { - console.log(' ⚠️ Could not pick a date'); - await ss(page, '08-booking-no-date'); - steps.push({ step: 'booking', status: 'PARTIAL', note: 'service selected, no date' }); - } - } catch (err) { - console.log(` ❌ ${err.message.slice(0, 120)}`); - steps.push({ step: 'booking', status: 'FAIL', error: err.message.slice(0, 80) }); - } - - // ─── STEP 3: Logout ────────────────────────────────────── - console.log('\n📍 Step 3: Logout'); - try { - await navigateTo(page, BASE_URL); - await page.waitForTimeout(2000); - - const logoutResult = await page.evaluate(() => { - const links = document.querySelectorAll('a, button'); - for (const l of links) { - const text = (l.textContent || '').trim().toLowerCase(); - if (text === 'выйти' || text === 'logout' || text === 'sign out' || text === 'выйти из аккаунта') { - l.click(); - return text; - } - } - return null; - }); - - if (logoutResult) { - console.log(` ✅ Clicked logout: "${logoutResult}"`); - await page.waitForTimeout(2000); - await ss(page, '09-logged-out'); - steps.push({ step: 'logout', status: 'PASS' }); - } else { - console.log(' ⚠️ No logout button (probably not logged in)'); - steps.push({ step: 'logout', status: 'SKIP', note: 'not logged in' }); - } - } catch (err) { - console.log(` ❌ ${err.message.slice(0, 120)}`); - steps.push({ step: 'logout', status: 'FAIL', error: err.message.slice(0, 80) }); - } - - // ─── STEP 4: Login ─────────────────────────────────────── - console.log('\n📍 Step 4: Login'); - try { - await navigateTo(page, BASE_URL); - await page.waitForTimeout(2000); - - // Make sure auth modal is open - await page.evaluate(() => { - const authLinks = document.querySelectorAll('a, button'); - for (const l of authLinks) { - if ((l.textContent || '').trim() === 'Войти' || (l.textContent || '').trim() === 'ВОЙТИ') { - l.click(); - break; - } - } - }); - await page.waitForTimeout(1000); - - // Click "Войти" tab - await page.click('a[href="#tab-login"]').catch(() => {}); - await page.evaluate(() => { - const links = document.querySelectorAll('a'); - for (const a of links) { - const text = (a.textContent || '').trim(); - if (text === 'Войти' && a.href?.includes('#tab-login')) { - a.click(); - break; - } - } - }); - await page.waitForTimeout(1500); - await ss(page, '10-login-tab'); - - // Fill login form - await page.fill('#login-email', TEST_EMAIL).catch(() => {}); - await page.fill('#login-password', TEST_PASSWORD).catch(() => {}); - console.log(` Filled: ${TEST_EMAIL} / *******`); - await ss(page, '11-login-filled'); - - // Click login button - const loginBtn = await page.evaluate(() => { - const btns = document.querySelectorAll('#tab-login button, .modal button'); - for (const b of btns) { - const text = (b.textContent || '').trim().toLowerCase(); - if (text.includes('войти') || text.includes('login') || text.includes('вход')) { - b.click(); - return text.slice(0, 60); - } - } - // Fallback: any submit in login tab - const submit = document.querySelector('#tab-login button[type="submit"], #tab-login .btn'); - if (submit) { submit.click(); return submit.textContent.trim().slice(0, 60); } - // Try modal submit - const modal = document.querySelector('.modal.show button:not([class*="close"])'); - if (modal) { modal.click(); return modal.textContent.trim().slice(0, 60); } - return null; - }); - - if (loginBtn) { - console.log(` Clicked: "${loginBtn}"`); - await page.waitForTimeout(3000); - await ss(page, '12-login-result'); - - const afterLogin = await page.evaluate(() => document.body?.innerText?.slice(0, 800) || ''); - if (afterLogin.includes('кабинет') || afterLogin.includes('выйти') || afterLogin.includes('профил')) { - console.log(' ✅ Login successful - cabinet/logout visible'); - steps.push({ step: 'login', status: 'PASS' }); - } else if (afterLogin.includes('неверн') || afterLogin.includes('ошибк') || afterLogin.includes('не найден')) { - console.log(' ⚠️ Login failed (wrong credentials - registration may not have completed)'); - steps.push({ step: 'login', status: 'PARTIAL', note: 'wrong credentials' }); - } else { - console.log(` Result: ${afterLogin.slice(0, 200)}`); - steps.push({ step: 'login', status: 'DONE', note: 'submitted' }); - } - } else { - console.log(' ⚠️ No login button'); - steps.push({ step: 'login', status: 'SKIP', note: 'no button' }); - } - } catch (err) { - console.log(` ❌ ${err.message.slice(0, 120)}`); - steps.push({ step: 'login', status: 'FAIL', error: err.message.slice(0, 80) }); - } - - // ─── STEP 5: Personal Cabinet ───────────────────────────── - console.log('\n📍 Step 5: Personal Cabinet'); - try { - // Try clicking "Войти в кабинет" link - const cabinetResult = await page.evaluate(() => { - const links = document.querySelectorAll('a, button'); - for (const l of links) { - const text = (l.textContent || '').trim().toLowerCase(); - if (text.includes('войти в кабинет') || text.includes('личный кабинет') || text.includes('мой кабинет') || text.includes('мои запис')) { - l.click(); - return text; - } - } - return null; - }); - - if (cabinetResult) { - console.log(` ✅ Clicked: "${cabinetResult}"`); - await page.waitForTimeout(3000); - await ss(page, '13-cabinet'); - - const cabinetText = await page.evaluate(() => document.body?.innerText?.slice(0, 1500) || ''); - console.log(` Cabinet preview: ${cabinetText.slice(0, 200)}`); - - // Look for appointments - const appointments = await page.evaluate(() => { - const appts = document.querySelectorAll('[class*="appointment"], [class*="booking"], [class*="record"], [class*="history"] li'); - return Array.from(appts).slice(0, 5).map(a => (a.textContent || '').trim().slice(0, 100)); - }); - if (appointments.length > 0) { - console.log(` Found ${appointments.length} appointments:`); - appointments.forEach(a => console.log(` → ${a}`)); - } - steps.push({ step: 'cabinet', status: 'PASS', appointments: appointments.length }); - } else { - console.log(' ⚠️ No cabinet link found'); - steps.push({ step: 'cabinet', status: 'SKIP', note: 'no cabinet link' }); - } - } catch (err) { - console.log(` ❌ ${err.message.slice(0, 120)}`); - steps.push({ step: 'cabinet', status: 'FAIL', error: err.message.slice(0, 80) }); - } - - await browser.close(); - - // ─── Report ────────────────────────────────────────────── - const report = { - timestamp: new Date().toISOString(), - targetUrl: BASE_URL, - testEmail: TEST_EMAIL, - steps, - summary: { - total: steps.length, - passed: steps.filter(s => s.status === 'PASS' || s.status === 'DONE').length, - partial: steps.filter(s => s.status === 'PARTIAL').length, - failed: steps.filter(s => s.status === 'FAIL').length, - skipped: steps.filter(s => s.status === 'SKIP').length, - }, - }; - - const rp = path.join(__dirname, '..', 'reports', 'e2e-booking-report.json'); - ensureDir(path.dirname(rp)); - fs.writeFileSync(rp, JSON.stringify(report, null, 2)); - - console.log('\n═══════════════════════════════════════════════════'); - console.log(' 📊 RESULTS'); - console.log(' ─────────────────────────────────────────────────'); - steps.forEach(s => { - const icon = s.status === 'PASS' ? '✅' : s.status === 'DONE' ? '✔️' : s.status === 'PARTIAL' ? '⚠️' : s.status === 'SKIP' ? '⏭️' : '❌'; - console.log(` ${icon} ${s.step}: ${s.status}${s.note ? ' (' + s.note + ')' : ''}${s.error ? ' - ' + s.error : ''}`); - }); - console.log(` 📄 Report: ${rp}`); - console.log('═══════════════════════════════════════════════════\n'); - - // ─── Gitea ─────────────────────────────────────────────── - if (GITEA_ISSUE) { - try { - const body = [ - '## 🧪 E2E Booking Flow: irina-vik.ru', - '', - `**Test email**: \`${TEST_EMAIL}\``, - '', - '| Step | Status | Details |', - '|------|--------|---------|', - ...steps.map(s => { - const icon = s.status === 'PASS' ? '✅' : s.status === 'DONE' ? '✔️' : s.status === 'PARTIAL' ? '⚠️' : s.status === 'SKIP' ? '⏭️' : '❌'; - return `| ${s.step} | ${icon} ${s.status} | ${s.note || s.error || s.service || ''} |`; - }), - ].join('\n'); - - const screens = fs.readdirSync(SCREENSHOT_DIR).filter(f => f.endsWith('.png')).map(f => path.join(SCREENSHOT_DIR, f)); - if (screens.length > 0) { - await gitea.uploadAndComment(GITEA_ISSUE, screens, body); - console.log(` ✅ Posted ${screens.length} screenshots to Gitea #${GITEA_ISSUE}`); - } else { - await gitea.postComment(GITEA_ISSUE, body); - console.log(` ✅ Posted comment to Gitea #${GITEA_ISSUE}`); - } - } catch (err) { - console.error(` ❌ Gitea: ${err.message}`); - } - } -} - -main().catch(err => { console.error('Fatal:', err); process.exit(1); }); \ No newline at end of file