diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1c46c2658 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# IDE +.idea/ + +# Python +__pycache__/ +*.py[cod] + +# Pytest / Allure +.pytest_cache/ +allure-results/ +.coverage +htmlcov/ + +# Venv +.venv/ +venv/ + +# OS +.DS_Store +Thumbs.db diff --git a/3_task/conftest.py b/3_task/conftest.py new file mode 100644 index 000000000..7911fde6c --- /dev/null +++ b/3_task/conftest.py @@ -0,0 +1,38 @@ +import pytest +from selenium import webdriver +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.firefox.options import Options as FirefoxOptions +from webdriver_manager.chrome import ChromeDriverManager +from webdriver_manager.firefox import GeckoDriverManager +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.firefox.service import Service as FirefoxService + + +BASE_URL = "https://stellarburgers.education-services.ru/" + + +@pytest.fixture(params=["chrome", "firefox"]) +def driver(request): + if request.param == "chrome": + options = ChromeOptions() + options.add_argument("--start-maximized") + + driver = webdriver.Chrome( + service=ChromeService(ChromeDriverManager().install()), + options=options + ) + else: + options = FirefoxOptions() + options.add_argument("--width=1920") + options.add_argument("--height=1080") + + driver = webdriver.Firefox( + service=FirefoxService(GeckoDriverManager().install()), + options=options + ) + + driver.implicitly_wait(5) + driver.get(BASE_URL) + + yield driver + driver.quit() diff --git a/3_task/pages/__init__.py b/3_task/pages/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/3_task/pages/base_page.py b/3_task/pages/base_page.py new file mode 100644 index 000000000..a25382a28 --- /dev/null +++ b/3_task/pages/base_page.py @@ -0,0 +1,65 @@ +import allure +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class BasePage: + + def __init__(self, driver, timeout=10): + self._driver = driver + self._wait = WebDriverWait(driver, timeout) + + # ---------- базовые ожидания ---------- + + @allure.step("Ожидание видимости элемента {locator}") + def wait_visible(self, locator): + return self._wait.until(EC.visibility_of_element_located(locator)) + + @allure.step("Ожидание кликабельности элемента {locator}") + def wait_clickable(self, locator): + return self._wait.until(EC.element_to_be_clickable(locator)) + + @allure.step("Ожидание присутствия элемента {locator}") + def wait_present(self, locator): + return self._wait.until(EC.presence_of_element_located(locator)) + + @allure.step("Ожидание исчезновения элемента {locator}") + def wait_invisible(self, locator): + self._wait.until(EC.invisibility_of_element_located(locator)) + + # ---------- действия ---------- + + @allure.step("Клик по элементу {locator}") + def click(self, locator): + element = self.wait_clickable(locator) + self._driver.execute_script( + "arguments[0].scrollIntoView({block: 'center'});", + element + ) + self._driver.execute_script("arguments[0].click();", element) + + @allure.step("Выполнение JS-скрипта") + def execute_js(self, script, *args): + self._driver.execute_script(script, *args) + + @allure.step("Обновление страницы") + def refresh_page(self): + self._driver.refresh() + + # ---------- получение данных ---------- + + @allure.step("Получение текста элемента {locator}") + def get_text(self, locator) -> str: + return self.wait_visible(locator).text + + @allure.step("Подсчёт элементов {locator}") + def count_elements(self, locator) -> int: + return len(self._driver.find_elements(*locator)) + + @allure.step("Проверка видимости элемента {locator}") + def is_visible(self, locator) -> bool: + try: + self.wait_visible(locator) + return True + except Exception: + return False diff --git a/3_task/pages/feed_page.py b/3_task/pages/feed_page.py new file mode 100644 index 000000000..467aa971c --- /dev/null +++ b/3_task/pages/feed_page.py @@ -0,0 +1,35 @@ +import allure +from selenium.webdriver.common.by import By +from pages.base_page import BasePage + + +class FeedPage(BasePage): + + ORDERS_TOTAL = ( + By.XPATH, + "//p[text()='Выполнено за все время:']/following-sibling::p" + ) + ORDERS_TODAY = ( + By.XPATH, + "//p[text()='Выполнено за сегодня:']/following-sibling::p" + ) + ORDERS_IN_PROGRESS = ( + By.XPATH, + "//ul[contains(@class,'OrderFeed_orderList')]" + ) + + @allure.step("Получение количества заказов за всё время") + def get_total_orders(self) -> int: + return int(self.get_text(self.ORDERS_TOTAL)) + + @allure.step("Получение количества заказов за сегодня") + def get_today_orders(self) -> int: + return int(self.get_text(self.ORDERS_TODAY)) + + @allure.step("Обновление страницы ленты заказов") + def refresh_feed(self): + self.refresh_page() + + @allure.step("Проверка наличия заказов в статусе «В работе»") + def has_orders_in_progress(self) -> bool: + return self.is_visible(self.ORDERS_IN_PROGRESS) diff --git a/3_task/pages/main_page.py b/3_task/pages/main_page.py new file mode 100644 index 000000000..40f4adb6b --- /dev/null +++ b/3_task/pages/main_page.py @@ -0,0 +1,92 @@ +import allure +from selenium.webdriver.common.by import By +from pages.base_page import BasePage + + +class MainPage(BasePage): + + CONSTRUCTOR_TAB = (By.XPATH, "//p[text()='Конструктор']") + FEED_TAB = (By.XPATH, "//a[contains(@href,'/feed')]") + + CONSTRUCTOR_HEADER = (By.XPATH, "//h1[text()='Соберите бургер']") + FEED_HEADER = (By.XPATH, "//h1[text()='Лента заказов']") + + INGREDIENT_NAME = "Соус Spicy-X" + INGREDIENT = (By.XPATH, f"//p[text()='{INGREDIENT_NAME}']") + + CONSTRUCTOR_SECTION = ( + By.XPATH, + "//section[contains(@class,'BurgerConstructor')]" + ) + + CONSTRUCTOR_ITEMS = ( + By.XPATH, + "//section[contains(@class,'BurgerConstructor')]//li" + ) + + MODAL = (By.XPATH, "//section[contains(@class,'Modal_modal')]") + MODAL_CLOSE_BUTTON = ( + By.XPATH, + "//button[contains(@class,'Modal_modal__close')]" + ) + + # ---------- навигация ---------- + + @allure.step("Открыть конструктор") + def open_constructor(self): + self.click(self.CONSTRUCTOR_TAB) + + @allure.step("Открыть ленту заказов") + def open_feed(self): + self.click(self.FEED_TAB) + + # ---------- проверки ---------- + + @allure.step("Проверить, что конструктор открыт") + def is_constructor_opened(self) -> bool: + return self.count_elements(self.CONSTRUCTOR_HEADER) > 0 + + @allure.step("Проверить, что лента заказов открыта") + def is_feed_opened(self) -> bool: + return self.count_elements(self.FEED_HEADER) > 0 + + # ---------- модалка ---------- + + @allure.step("Открыть модальное окно ингредиента") + def open_ingredient_modal(self): + self.click(self.INGREDIENT) + self.wait_visible(self.MODAL) + + @allure.step("Закрыть модальное окно ингредиента") + def close_ingredient_modal(self): + self.click(self.MODAL_CLOSE_BUTTON) + + @allure.step("Дождаться закрытия модального окна") + def wait_modal_closed(self): + self.wait_invisible(self.MODAL) + + # ---------- конструктор ---------- + + @allure.step("Добавить ингредиент в конструктор") + def add_ingredient_to_constructor(self): + ingredient = self.wait_present(self.INGREDIENT) + target = self.wait_present(self.CONSTRUCTOR_SECTION) + + self.execute_js( + """ + const source = arguments[0]; + const target = arguments[1]; + const dataTransfer = new DataTransfer(); + + source.dispatchEvent(new DragEvent('dragstart', { dataTransfer })); + target.dispatchEvent(new DragEvent('dragover', { dataTransfer })); + target.dispatchEvent(new DragEvent('drop', { dataTransfer })); + source.dispatchEvent(new DragEvent('dragend', { dataTransfer })); + """, + ingredient, + target + ) + + @allure.step("Проверить, что в конструкторе есть ингредиенты") + def constructor_has_items(self) -> bool: + return self.count_elements(self.CONSTRUCTOR_ITEMS) > 0 diff --git a/3_task/requirements.txt b/3_task/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/3_task/tests/__init__.py b/3_task/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/3_task/tests/test_feed.py b/3_task/tests/test_feed.py new file mode 100644 index 000000000..2f0d9ff5a --- /dev/null +++ b/3_task/tests/test_feed.py @@ -0,0 +1,53 @@ +import allure + +from pages.feed_page import FeedPage +from pages.main_page import MainPage + + +@allure.feature("Лента заказов") +class TestFeed: + + @allure.title("Отображение и обновление счётчиков заказов в ленте") + def test_orders_counters_display_and_update(self, driver): + main_page = MainPage(driver) + feed_page = FeedPage(driver) + + with allure.step("Переход в конструктор"): + main_page.open_constructor() + + with allure.step("Добавление ингредиента в конструктор"): + main_page.add_ingredient_to_constructor() + assert main_page.constructor_has_items() + + with allure.step("Переход в ленту заказов"): + main_page.open_feed() + + with allure.step("Фиксация текущих значений счётчиков"): + total_before = feed_page.get_total_orders() + + with allure.step("Обновление ленты заказов"): + feed_page.refresh_feed() + + with allure.step("Проверка, что счётчик выполненных заказов не уменьшился"): + assert feed_page.get_total_orders() >= total_before + + @allure.title("В ленте заказов отображается раздел «В работе»") + def test_orders_in_progress_section_displayed(self, driver): + main_page = MainPage(driver) + feed_page = FeedPage(driver) + + with allure.step("Переход в конструктор"): + main_page.open_constructor() + + with allure.step("Добавление ингредиента в конструктор"): + main_page.add_ingredient_to_constructor() + assert main_page.constructor_has_items() + + with allure.step("Переход в ленту заказов"): + main_page.open_feed() + + with allure.step("Обновление ленты заказов"): + feed_page.refresh_feed() + + with allure.step("Проверка наличия раздела «В работе»"): + assert feed_page.has_orders_in_progress() diff --git a/3_task/tests/test_main_functional.py b/3_task/tests/test_main_functional.py new file mode 100644 index 000000000..4224f7bd6 --- /dev/null +++ b/3_task/tests/test_main_functional.py @@ -0,0 +1,48 @@ +import allure + +from pages.main_page import MainPage + + +@allure.feature("Главная страница") +class TestMainFunctional: + + @allure.title("Переход по клику на вкладку «Конструктор»") + def test_open_constructor(self, driver): + main_page = MainPage(driver) + + with allure.step("Клик по вкладке «Конструктор»"): + main_page.open_constructor() + + with allure.step("Проверка открытия конструктора"): + assert main_page.is_constructor_opened() + + @allure.title("Переход по клику на вкладку «Лента заказов»") + def test_open_feed(self, driver): + main_page = MainPage(driver) + + with allure.step("Клик по вкладке «Лента заказов»"): + main_page.open_feed() + + with allure.step("Проверка открытия ленты заказов"): + assert main_page.is_feed_opened() + + @allure.title("Открытие и закрытие модального окна ингредиента") + def test_ingredient_modal_open_and_close(self, driver): + main_page = MainPage(driver) + + with allure.step("Открытие модального окна ингредиента"): + main_page.open_ingredient_modal() + + with allure.step("Закрытие модального окна ингредиента"): + main_page.close_ingredient_modal() + main_page.wait_modal_closed() + + @allure.title("Ингредиент добавляется в конструктор") + def test_ingredient_added_to_constructor(self, driver): + main_page = MainPage(driver) + + with allure.step("Добавление ингредиента в конструктор"): + main_page.add_ingredient_to_constructor() + + with allure.step("Проверка наличия ингредиента в конструкторе"): + assert main_page.constructor_has_items()