""" Настоящий E2E тест для удаления сообщества через браузер. Использует Playwright для автоматизации браузера и тестирует: 1. Запуск сервера 2. Открытие админ-панели в браузере 3. Авторизацию 4. Переход на страницу сообществ 5. Удаление сообщества 6. Проверку результата """ import pytest import time import asyncio from playwright.async_api import async_playwright, Page, Browser, BrowserContext import subprocess import signal import os import sys import requests from dotenv import load_dotenv # Загружаем переменные окружения для E2E тестов load_dotenv() # Добавляем путь к проекту для импорта sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from auth.orm import Author from orm.community import Community, CommunityAuthor from services.db import local_session class TestCommunityDeleteE2EBrowser: """E2E тесты для удаления сообщества через браузер""" @pytest.fixture async def browser_setup(self): """Настройка браузера и запуск серверов""" # Запускаем бэкенд сервер в фоне backend_process = None frontend_process = None try: # Проверяем, не запущен ли уже сервер try: response = requests.get("http://localhost:8000/", timeout=2) if response.status_code == 200: print("✅ Бэкенд сервер уже запущен") backend_running = True else: backend_running = False except: backend_running = False if not backend_running: # Запускаем бэкенд сервер print("🔄 Запускаем бэкенд сервер...") backend_process = subprocess.Popen( ["python3", "dev.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) # Ждем запуска бэкенда print("⏳ Ждем запуска бэкенда...") for i in range(30): # Ждем максимум 30 секунд try: response = requests.get("http://localhost:8000/", timeout=2) if response.status_code == 200: print("✅ Бэкенд сервер запущен") break except: pass await asyncio.sleep(1) else: raise Exception("Бэкенд сервер не запустился за 30 секунд") # Проверяем фронтенд try: response = requests.get("http://localhost:3000", timeout=2) if response.status_code == 200: print("✅ Фронтенд сервер уже запущен") frontend_running = True else: frontend_running = False except: frontend_running = False if not frontend_running: # Запускаем фронтенд сервер print("🔄 Запускаем фронтенд сервер...") frontend_process = subprocess.Popen( ["npm", "run", "dev"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) # Ждем запуска фронтенда print("⏳ Ждем запуска фронтенда...") for i in range(60): # Ждем максимум 60 секунд try: response = requests.get("http://localhost:3000", timeout=2) if response.status_code == 200: print("✅ Фронтенд сервер запущен") break except: pass await asyncio.sleep(1) else: raise Exception("Фронтенд сервер не запустился за 60 секунд") # Запускаем браузер print("🔄 Запускаем браузер...") playwright = await async_playwright().start() browser = await playwright.chromium.launch( headless=False, # Оставляем headless=False для отладки E2E тестов args=["--no-sandbox", "--disable-dev-shm-usage"] ) context = await browser.new_context() page = await context.new_page() yield { "playwright": playwright, "browser": browser, "context": context, "page": page, "backend_process": backend_process, "frontend_process": frontend_process } finally: # Очистка print("🧹 Очистка ресурсов...") if frontend_process: frontend_process.terminate() try: frontend_process.wait(timeout=5) except subprocess.TimeoutExpired: frontend_process.kill() if backend_process: backend_process.terminate() try: backend_process.wait(timeout=5) except subprocess.TimeoutExpired: backend_process.kill() try: if 'browser' in locals(): await browser.close() if 'playwright' in locals(): await playwright.stop() except Exception as e: print(f"⚠️ Ошибка при закрытии браузера: {e}") @pytest.fixture def test_community_for_browser(self, db_session, test_users): """Создает тестовое сообщество для удаления через браузер""" community = Community( id=888, name="Browser Test Community", slug="browser-test-community", desc="Test community for browser E2E tests", created_by=test_users[0].id, created_at=int(time.time()) ) db_session.add(community) db_session.commit() return community @pytest.fixture def admin_user_for_browser(self, db_session, test_users, test_community_for_browser): """Создает администратора с правами на удаление""" user = test_users[0] # Создаем CommunityAuthor с правами администратора ca = CommunityAuthor( community_id=test_community_for_browser.id, author_id=user.id, roles="admin,editor,author" ) db_session.add(ca) db_session.commit() return user async def test_community_delete_browser_workflow(self, browser_setup, test_users): """Полный E2E тест удаления сообщества через браузер""" page = browser_setup["page"] # Используем существующее сообщество для тестирования удаления test_community_name = "Test Admin Community" # Существующее сообщество из БД test_community_slug = "test-admin-community-test-7674853a" # Конкретный slug для удаления (ID=13) print(f"🔍 Будем тестировать удаление сообщества: {test_community_name}") try: # 1. Открываем админ-панель на порту 3000 print("🌐 Открываем админ-панель...") await page.goto("http://localhost:3000") # Ждем загрузки страницы и JavaScript await page.wait_for_load_state("networkidle") await page.wait_for_load_state("domcontentloaded") # Дополнительное ожидание для загрузки React приложения await page.wait_for_timeout(3000) print("✅ Страница загружена") # 2. Авторизуемся через форму входа print("🔐 Авторизуемся через форму входа...") # Ждем появления формы входа с увеличенным таймаутом await page.wait_for_selector('input[type="email"]', timeout=30000) await page.wait_for_selector('input[type="password"]', timeout=10000) # Заполняем форму входа await page.fill('input[type="email"]', 'test_admin@discours.io') await page.fill('input[type="password"]', 'password123') # Нажимаем кнопку входа await page.click('button[type="submit"]') # Ждем успешной авторизации (редирект на главную страницу админки) await page.wait_for_url("http://localhost:3000/admin/**", timeout=10000) print("✅ Авторизация успешна") # Проверяем что мы действительно в админ-панели await page.wait_for_selector('button:has-text("Сообщества")', timeout=30000) print("✅ Админ-панель загружена") # 3. Переходим на страницу сообществ print("📋 Переходим на страницу сообществ...") # Ищем кнопку "Сообщества" в навигации await page.wait_for_selector('button:has-text("Сообщества")', timeout=30000) await page.click('button:has-text("Сообщества")') # Ждем загрузки страницы сообществ await page.wait_for_load_state("networkidle") print("✅ Страница сообществ загружена") # Проверяем что мы на правильной странице current_url = page.url print(f"📍 Текущий URL: {current_url}") if "/admin/communities" not in current_url: print("⚠️ Не на странице управления сообществами, переходим...") await page.goto("http://localhost:3000/admin/communities") await page.wait_for_load_state("networkidle") print("✅ Перешли на страницу управления сообществами") # 4. Ищем наше тестовое сообщество print(f"🔍 Ищем сообщество: {test_community_name}") # Ждем появления таблицы сообществ await page.wait_for_selector('table', timeout=10000) print("✅ Таблица сообществ найдена") # Ждем загрузки данных await page.wait_for_selector('table tbody tr', timeout=10000) print("✅ Данные в таблице загружены") # Ищем строку с нашим конкретным сообществом по slug community_row = await page.wait_for_selector( f'table tbody tr:has-text("{test_community_slug}")', timeout=10000 ) if not community_row: # Делаем скриншот для отладки await page.screenshot(path="test-results/communities_table.png") # Получаем список всех сообществ в таблице all_communities = await page.evaluate(""" () => { const rows = document.querySelectorAll('table tbody tr'); return Array.from(rows).map(row => { const cells = row.querySelectorAll('td'); return { id: cells[0]?.textContent?.trim(), name: cells[1]?.textContent?.trim(), slug: cells[2]?.textContent?.trim() }; }); } """) print(f"📋 Найденные сообщества в таблице: {all_communities}") raise Exception(f"Сообщество {test_community_name} не найдено в таблице") print(f"✅ Найдено сообщество: {test_community_name}") # 5. Удаляем сообщество print("🗑️ Удаляем сообщество...") # Ищем кнопку удаления в строке с нашим конкретным сообществом # Кнопка удаления содержит символ '×' и находится в последней ячейке delete_button = await page.wait_for_selector( f'table tbody tr:has-text("{test_community_slug}") button:has-text("×")', timeout=10000 ) if not delete_button: # Альтернативный поиск - найти кнопку в последней ячейке строки delete_button = await page.wait_for_selector( f'table tbody tr:has-text("{test_community_slug}") td:last-child button', timeout=10000 ) if not delete_button: # Еще один способ - найти кнопку по CSS модулю классу delete_button = await page.wait_for_selector( f'table tbody tr:has-text("{test_community_slug}") button[class*="delete-button"]', timeout=10000 ) if not delete_button: # Делаем скриншот для отладки await page.screenshot(path="test-results/delete_button_not_found.png") raise Exception("Кнопка удаления не найдена") print("✅ Кнопка удаления найдена") # Нажимаем кнопку удаления await delete_button.click() # Ждем появления диалога подтверждения # Модальное окно использует CSS модули, поэтому ищем по backdrop await page.wait_for_selector('[class*="backdrop"]', timeout=10000) # Подтверждаем удаление # Ищем кнопку "Удалить" в модальном окне confirm_button = await page.wait_for_selector( '[class*="backdrop"] button:has-text("Удалить")', timeout=10000 ) if not confirm_button: # Альтернативный поиск confirm_button = await page.wait_for_selector( '[class*="modal"] button:has-text("Удалить")', timeout=10000 ) if not confirm_button: # Еще один способ - найти кнопку с variant="danger" confirm_button = await page.wait_for_selector( '[class*="backdrop"] button[class*="danger"]', timeout=10000 ) if not confirm_button: # Делаем скриншот для отладки await page.screenshot(path="test-results/confirm_button_not_found.png") raise Exception("Кнопка подтверждения не найдена") print("✅ Кнопка подтверждения найдена") await confirm_button.click() # Ждем исчезновения диалога и обновления страницы await page.wait_for_load_state("networkidle") print("✅ Сообщество удалено") # Ждем исчезновения модального окна try: await page.wait_for_selector('[class*="backdrop"]', timeout=5000, state='hidden') print("✅ Модальное окно закрылось") except: print("⚠️ Модальное окно не закрылось автоматически") # Ждем обновления таблицы await page.wait_for_timeout(3000) # Ждем 3 секунды для обновления # 6. Проверяем что сообщество действительно удалено print("🔍 Проверяем что сообщество удалено...") # Ждем немного для обновления списка await asyncio.sleep(2) # Проверяем что конкретное сообщество больше не отображается в таблице community_still_exists = await page.query_selector(f'table tbody tr:has-text("{test_community_slug}")') if community_still_exists: # Попробуем обновить страницу и проверить еще раз print("🔄 Обновляем страницу и проверяем еще раз...") await page.reload() await page.wait_for_load_state("networkidle") await page.wait_for_selector('table tbody tr', timeout=10000) # Проверяем еще раз после обновления community_still_exists = await page.query_selector(f'table tbody tr:has-text("{test_community_slug}")') if community_still_exists: # Делаем скриншот для отладки await page.screenshot(path="test-results/community_still_exists.png") # Получаем список всех сообществ для отладки all_communities = await page.evaluate(""" () => { const rows = document.querySelectorAll('table tbody tr'); return Array.from(rows).map(row => { const cells = row.querySelectorAll('td'); return { id: cells[0]?.textContent?.trim(), name: cells[1]?.textContent?.trim(), slug: cells[2]?.textContent?.trim() }; }); } """) print(f"📋 Сообщества в таблице после обновления: {all_communities}") raise Exception(f"Сообщество {test_community_name} (slug: {test_community_slug}) все еще отображается после удаления и обновления страницы") else: print("✅ Сообщество удалено после обновления страницы") print("✅ Сообщество действительно удалено из списка") # 7. Делаем скриншот результата await page.screenshot(path="test-results/community_deleted_success.png") print("📸 Скриншот сохранен: test-results/community_deleted_success.png") print("🎉 E2E тест удаления сообщества прошел успешно!") except Exception as e: print(f"❌ Ошибка в E2E тесте: {e}") # Делаем скриншот при ошибке try: await page.screenshot(path=f"test-results/error_{int(time.time())}.png") print("📸 Скриншот ошибки сохранен") except Exception as screenshot_error: print(f"⚠️ Не удалось сделать скриншот при ошибке: {screenshot_error}") raise async def test_community_delete_without_permissions_browser(self, browser_setup, test_community_for_browser): """Тест попытки удаления без прав через браузер""" page = browser_setup["page"] try: # 1. Открываем админ-панель print("🔄 Открываем админ-панель...") await page.goto("http://localhost:3000/admin") await page.wait_for_load_state("networkidle") # 2. Авторизуемся как обычный пользователь (без прав admin) print("🔐 Авторизуемся как обычный пользователь...") import os regular_username = os.getenv("TEST_REGULAR_USERNAME", "user2@example.com") password = os.getenv("E2E_TEST_PASSWORD", "password123") await page.fill("input[type='email']", regular_username) await page.fill("input[type='password']", password) await page.click("button[type='submit']") await page.wait_for_load_state("networkidle") # 3. Переходим на страницу сообществ print("🏘️ Переходим на страницу сообществ...") await page.click("a[href='/admin/communities']") await page.wait_for_load_state("networkidle") # 4. Ищем сообщество print(f"🔍 Ищем сообщество: {test_community_for_browser.name}") community_row = await page.wait_for_selector( f"tr:has-text('{test_community_for_browser.name}')", timeout=10000 ) if not community_row: print("❌ Сообщество не найдено") await page.screenshot(path="test-results/community_not_found_no_permissions.png") raise Exception("Сообщество не найдено") # 5. Проверяем что кнопка удаления недоступна или отсутствует print("🔒 Проверяем доступность кнопки удаления...") delete_button = await community_row.query_selector("button:has-text('Удалить')") if delete_button: # Если кнопка есть, пробуем нажать и проверяем ошибку print("⚠️ Кнопка удаления найдена, пробуем нажать...") await delete_button.click() # Ждем появления ошибки await page.wait_for_selector("[role='alert']", timeout=5000) error_message = await page.text_content("[role='alert']") if "Недостаточно прав" in error_message or "permission" in error_message.lower(): print("✅ Ошибка доступа получена корректно") else: print(f"❌ Неожиданная ошибка: {error_message}") await page.screenshot(path="test-results/unexpected_error.png") raise Exception(f"Неожиданная ошибка: {error_message}") else: print("✅ Кнопка удаления недоступна (как и должно быть)") # 6. Проверяем что сообщество осталось в БД print("🗄️ Проверяем что сообщество осталось в БД...") with local_session() as session: community = session.query(Community).filter_by( slug=test_community_for_browser.slug ).first() if not community: print("❌ Сообщество было удалено без прав") raise Exception("Сообщество было удалено без соответствующих прав") print("✅ Сообщество осталось в БД (как и должно быть)") print("🎉 E2E тест проверки прав доступа прошел успешно!") except Exception as e: try: await page.screenshot(path=f"test-results/error_permissions_{int(time.time())}.png") except: print("⚠️ Не удалось сделать скриншот при ошибке") print(f"❌ Ошибка в E2E тесте прав доступа: {e}") raise async def test_community_delete_ui_validation(self, browser_setup, test_community_for_browser, admin_user_for_browser): """Тест UI валидации при удалении сообщества""" page = browser_setup["page"] try: # 1. Авторизуемся как админ print("🔐 Авторизуемся как админ...") await page.goto("http://localhost:3000/admin") await page.wait_for_load_state("networkidle") import os username = os.getenv("E2E_TEST_USERNAME", "test_admin@discours.io") password = os.getenv("E2E_TEST_PASSWORD", "password123") await page.fill("input[type='email']", username) await page.fill("input[type='password']", password) await page.click("button[type='submit']") await page.wait_for_load_state("networkidle") # 2. Переходим на страницу сообществ print("🏘️ Переходим на страницу сообществ...") await page.click("a[href='/admin/communities']") await page.wait_for_load_state("networkidle") # 3. Ищем сообщество и нажимаем удаление print(f"🔍 Ищем сообщество: {test_community_for_browser.name}") community_row = await page.wait_for_selector( f"tr:has-text('{test_community_for_browser.name}')", timeout=10000 ) delete_button = await community_row.query_selector("button:has-text('Удалить')") await delete_button.click() # 4. Проверяем модальное окно print("⚠️ Проверяем модальное окно...") modal = await page.wait_for_selector("[role='dialog']", timeout=10000) # Проверяем текст предупреждения modal_text = await modal.text_content() if "удалить" not in modal_text.lower() and "delete" not in modal_text.lower(): print(f"❌ Неожиданный текст в модальном окне: {modal_text}") await page.screenshot(path="test-results/unexpected_modal_text.png") raise Exception("Неожиданный текст в модальном окне") # 5. Отменяем удаление print("❌ Отменяем удаление...") cancel_button = await page.query_selector("button:has-text('Отмена')") if not cancel_button: cancel_button = await page.query_selector("button:has-text('Cancel')") if cancel_button: await cancel_button.click() # Проверяем что модальное окно закрылось await page.wait_for_selector("[role='dialog']", state="hidden", timeout=5000) # Проверяем что сообщество осталось в таблице community_still_exists = await page.query_selector( f"tr:has-text('{test_community_for_browser.name}')" ) if not community_still_exists: print("❌ Сообщество исчезло после отмены") await page.screenshot(path="community_disappeared_after_cancel.png") raise Exception("Сообщество исчезло после отмены удаления") print("✅ Сообщество осталось после отмены") else: print("⚠️ Кнопка отмены не найдена") print("🎉 E2E тест UI валидации прошел успешно!") except Exception as e: try: await page.screenshot(path=f"test-results/error_ui_validation_{int(time.time())}.png") except: print("⚠️ Не удалось сделать скриншот при ошибке") print(f"❌ Ошибка в E2E тесте UI валидации: {e}") raise