From e3138f2478577fd6263c395ed4fc1f00db5b9880 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Tue, 3 Feb 2026 00:44:34 +0300 Subject: [PATCH] Sprint 6: tests + allure --- .gitignore | 12 ++++ README.md | Bin 26 -> 1948 bytes conftest.py | 13 +++++ data.py | 20 +++++++ locators/main_page_locators.py | 21 +++++++ locators/order_page_locators.py | 30 ++++++++++ pages/base_page.py | 44 ++++++++++++++ pages/main_page.py | 46 +++++++++++++++ pages/order_page.py | 66 +++++++++++++++++++++ requirements.txt | 27 +++++++++ tests/test_faq.py | 23 ++++++++ tests/test_order.py | 100 ++++++++++++++++++++++++++++++++ urls.py | 7 +++ 13 files changed, 409 insertions(+) create mode 100644 .gitignore create mode 100644 conftest.py create mode 100644 data.py create mode 100644 locators/main_page_locators.py create mode 100644 locators/order_page_locators.py create mode 100644 pages/base_page.py create mode 100644 pages/main_page.py create mode 100644 pages/order_page.py create mode 100644 requirements.txt create mode 100644 tests/test_faq.py create mode 100644 tests/test_order.py create mode 100644 urls.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0326757 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Python +.venv/ +__pycache__/ +*.pyc + +# Allure +allure_results/ +allure_report/ + +# IDE +.vscode/ +.idea/ diff --git a/README.md b/README.md index 895b479f1fbac88f91662c40e48e44bac0424341..5ee2c1a5aeb2b242ba0be6d734ea2662b4a4a126 100644 GIT binary patch literal 1948 zcmbuANpBND5QXc|CnO|(!XU0>GqG_Tdu(TMKnX&CI9nzOA;J#Cb`bF6f$vqP?Vk7$ z1RBlO)Aj1rt7U%wdSZpWv#&O_OS`hU4Xt4h?SVZCwXhdT&U)x`Jr0F!hu!d+dp~T3 z9o9B^e$KVUseAm=#!4%)uCewB)Hxnz*0K$bYa7`bZ>RQ!V{Yg8UUvo~`^@_#*BhJo z9)0`9$|<{M?3(gr1IEJL3O&5+Wvvi{wpQe=-|*2AvF zecv$(>vAu_&>HbztDPf9x^ax5UuXCq!*RxrHO@X(4fo#&O>7I3Vn?h*#e@*;BjOk) zr{K=HPZua%e0E(kAr^MP`wm)fhBl~$?PiLKr=k}>F_?g+%nea+WEVs`#Anru)ctYG zxQeB$K66jC&;faecV%13cJMv$yeD7ceCGN3cWfc1D>2U?i#5l)H{gb+> zNpA@=c6;n8@jD~NIcILGn(A;IdsU>IN&&L>Q7<4wL>C5U?@fd`o85N^=kKYsbuQ_)OQk-}b-Zafq2dTU3 z;RV-N?P6bZUQLinM{WjjS!ae^VyT&O!KtjpNUDs|TVf&|mQ^NpPq7nk{ReutdWuD1 z9Wv2?OUbjo_nAO-R+@0qf8&I8;mNw~k?!$L>+2q?cpf?Kmd__e(*;vGj{6GBt2Of1 z3{2Icl{GT2KSi9+XRchQvbH+&W3F@GT{y;iEf>C*|A6Q@_fz;h(qHWyeey4Nz5M~r CY6-&t delta 5 McmbQkFEv3500h+mZ~y=R diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..a53d785 --- /dev/null +++ b/conftest.py @@ -0,0 +1,13 @@ +import pytest +from selenium import webdriver +from selenium.webdriver.firefox.options import Options + +@pytest.fixture(scope="function") +def driver(): + options = Options() + options.add_argument("--width=1920") + options.add_argument("--height=1080") + browser = webdriver.Firefox(options=options) + yield browser + browser.quit() + \ No newline at end of file diff --git a/data.py b/data.py new file mode 100644 index 0000000..d66b131 --- /dev/null +++ b/data.py @@ -0,0 +1,20 @@ +class OrderData: + param = 'first_name, last_name, address, metro_station, phone, delivery_date, rental_period' + value = [ + ['Евгений', 'Петров', 'Каланчевская, 15', 'Комсомольская', '+78002228888', '11', 'двое суток'], + ['Илья', 'Ильф', 'Брянская, 5', 'Киевская', '+79851150102', '10', 'трое суток'] + ] + + +class FAQData: + param = 'number, expected_answer' + value = [ + (0, "Сутки — 400 рублей. Оплата курьеру — наличными или картой."), + (1, "Пока что у нас так: один заказ — один самокат. Если хотите покататься с друзьями, можете просто сделать несколько заказов — один за другим."), + (2, "Допустим, вы оформляете заказ на 8 мая. Мы привозим самокат 8 мая в течение дня. Отсчёт времени аренды начинается с момента, когда вы оплатите заказ курьеру. Если мы привезли самокат 8 мая в 20:30, суточная аренда закончится 9 мая в 20:30."), + (3, "Только начиная с завтрашнего дня. Но скоро станем расторопнее."), + (4, "Пока что нет! Но если что-то срочное — всегда можно позвонить в поддержку по красивому номеру 1010."), + (5, "Самокат приезжает к вам с полной зарядкой. Этого хватает на восемь суток — даже если будете кататься без передышек и во сне. Зарядка не понадобится."), + (6, "Да, пока самокат не привезли. Штрафа не будет, объяснительной записки тоже не попросим. Все же свои."), + (7, "Да, обязательно. Всем самокатов! И Москве, и Московской области.") + ] diff --git a/locators/main_page_locators.py b/locators/main_page_locators.py new file mode 100644 index 0000000..8b59a90 --- /dev/null +++ b/locators/main_page_locators.py @@ -0,0 +1,21 @@ +from selenium.webdriver.common.by import By + + +class MainPageLocators: + + @staticmethod + def question_locator(question_index: int): + return (By.ID, f"accordion__heading-{question_index}") + + @staticmethod + def answer_locator(answer_index: int): + return (By.ID, f"accordion__panel-{answer_index}") + + + + TOP_ORDER_BTN = (By.XPATH, "//div[contains(@class, 'Header_Nav')]/button[contains(@class, 'Button_Button')]") + BOTTOM_ORDER_BTN = (By.XPATH, "//div[contains(@class, 'Home_FinishButton')]/button[contains(@class, 'Button_Button')]") + FAQ_SECTION = (By.CLASS_NAME, "Home_FAQ__3uVm4") + SCOOTER_LOGO = (By.XPATH, "//img[@alt='Scooter']") + YANDEX_LOGO = (By.XPATH, "//img[@alt='Yandex']") + DZEN_NEWS = (By.XPATH, "//div[text() = 'Новости']") diff --git a/locators/order_page_locators.py b/locators/order_page_locators.py new file mode 100644 index 0000000..2363c91 --- /dev/null +++ b/locators/order_page_locators.py @@ -0,0 +1,30 @@ +from selenium.webdriver.common.by import By + +class OrderPageLocators: + + NAME_INPUT = (By.XPATH, "//input[@placeholder='* Имя']") + LASTNAME_INPUT = (By.XPATH, "//input[@placeholder='* Фамилия']") + ADDRESS_INPUT = (By.XPATH, "//input[@placeholder='* Адрес: куда привезти заказ']") + METRO_FIELD = (By.XPATH, "//input[@placeholder='* Станция метро']") + PHONE_FIELD = (By.XPATH, "//input[@placeholder='* Телефон: на него позвонит курьер']") + + NEXT_BTN = (By.XPATH, "//button[text()='Далее']") + DATE_INPUT = (By.XPATH, "//input[@placeholder='* Когда привезти самокат']") + RENTAL_PERIOD_DROPDOWN = (By.XPATH, "//div[text()='* Срок аренды']") + ORDER_BTN = (By.XPATH, "//div[contains(@class, 'Order_Buttons')]//button[text()='Заказать']") + CONFIRM_BTN = (By.XPATH, "//div[contains(@class, 'Order_Modal')]//button[text()='Да']") + STATUS_BTN = (By.XPATH, "//button[text()='Посмотреть статус']") + + COOKIE_BTN = (By.ID, "rcc-confirm-button") + + @staticmethod + def period_locator(period: str): + return (By.XPATH, f"//div[contains(text(),'{period}')]") + + @staticmethod + def date_locator(day_number: str): + return (By.XPATH, f"//div[contains(text(),'{day_number}')]") + + @staticmethod + def station_locator(station: str): + return (By.XPATH, f"//div[contains(text(),'{station}')]") \ No newline at end of file diff --git a/pages/base_page.py b/pages/base_page.py new file mode 100644 index 0000000..bc2e6f7 --- /dev/null +++ b/pages/base_page.py @@ -0,0 +1,44 @@ +import allure +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait +from locators.order_page_locators import OrderPageLocators + + + +class BasePage: + + def __init__(self, driver): + self.driver = driver + + @allure.step('Закрыть cookies-баннер') + def close_cookie_banner(self): + try: + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(OrderPageLocators.COOKIE_BTN) + ).click() + except: + pass + + @allure.step('Открыть страницу') + def open_page(self, url): + self.driver.get(url) + self.close_cookie_banner() + + @allure.step('Поиск элемента') + def wait_and_find_element(self, locator, timeout=10): + element = WebDriverWait(self.driver, timeout).until(EC.visibility_of_element_located(locator)) + return element + + @allure.step('Скролл до элемента') + def scroll_to_element(self, locator): + element = self.driver.find_element(*locator) + self.driver.execute_script("arguments[0].scrollIntoView();", element) + + @allure.step("Переключиться на окно {window_index}") + def switch_to_window(self, window_index: int): + WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > window_index) + self.driver.switch_to.window(self.driver.window_handles[window_index]) + + @allure.step('Получить текущий URL') + def get_current_url(self): + return self.driver.current_url diff --git a/pages/main_page.py b/pages/main_page.py new file mode 100644 index 0000000..8ec5f86 --- /dev/null +++ b/pages/main_page.py @@ -0,0 +1,46 @@ +from locators.main_page_locators import MainPageLocators +from pages.base_page import BasePage +from urls import Urls +import allure + + +class MainPage(BasePage): + + @allure.step("Клик по верхней кнопке заказа") + def click_top_order_btn(self): + self.wait_and_find_element(MainPageLocators.TOP_ORDER_BTN).click() + + @allure.step("Клик по нижней кнопке заказа") + def click_bottom_order_btn(self): + self.scroll_to_element(MainPageLocators.BOTTOM_ORDER_BTN) + self.wait_and_find_element(MainPageLocators.BOTTOM_ORDER_BTN).click() + + @allure.step("Скролл к разделу FAQ") + def scroll_to_faq(self): + self.scroll_to_element(MainPageLocators.FAQ_SECTION) + + @allure.step("Клик по логотипу Яндекса") + def click_to_yandex_logo(self): + self.wait_and_find_element(MainPageLocators.YANDEX_LOGO).click() + + @allure.step("Клик по лого самоката") + def click_to_scooter(self): + self.wait_and_find_element(MainPageLocators.SCOOTER_LOGO).click() + + @allure.step("Получаем текст вопроса и ответ по локаторам") + def get_question_and_answer(self, question_index, answer_index): + self.scroll_to_faq() + question_loc = MainPageLocators.question_locator(question_index) + answer_loc = MainPageLocators.answer_locator(answer_index) + question_text = self.wait_and_find_element(question_loc).text + self.wait_and_find_element(question_loc).click() + answer_text = self.wait_and_find_element(answer_loc).text + return question_text, answer_text + + @allure.step("Проверка редиректа на страницу заказа") + def check_redirect_to_order_page(self): + return self.get_current_url() == Urls.ORDER_PAGE + + @allure.step("Дзен новости") + def dzen_news_(self): + return self.wait_and_find_element(MainPageLocators.DZEN_NEWS) diff --git a/pages/order_page.py b/pages/order_page.py new file mode 100644 index 0000000..34a7f8a --- /dev/null +++ b/pages/order_page.py @@ -0,0 +1,66 @@ +import allure + +from pages.base_page import BasePage +from locators.order_page_locators import OrderPageLocators + + +class OrderPage(BasePage): + @allure.step('Заполняем поле Имя') + def set_name(self, name): + self.wait_and_find_element(OrderPageLocators.NAME_INPUT).send_keys(name) + + @allure.step('Заполняем поле Фамилия') + def set_second_name(self, lastname): + self.wait_and_find_element(OrderPageLocators.LASTNAME_INPUT).send_keys(lastname) + + @allure.step('Заполняем поле Адрес') + def set_address(self, address): + self.wait_and_find_element(OrderPageLocators.ADDRESS_INPUT).send_keys(address) + + @allure.step('Заполняем поле Станция метро') + def set_metro_station(self, metro_station): + self.wait_and_find_element(OrderPageLocators.METRO_FIELD).click() + self.wait_and_find_element(OrderPageLocators.station_locator(metro_station)).click() + + @allure.step('Заполняем поле Номер телефона') + def set_phone_number(self, phone): + self.wait_and_find_element(OrderPageLocators.PHONE_FIELD).send_keys(phone) + + @allure.step('Кликаем кнопку далее') + def click_continue_button(self): + self.wait_and_find_element(OrderPageLocators.NEXT_BTN).click() + + @allure.step('Выбираем дату доставки') + def set_delivery_date(self, delivery_day): + self.wait_and_find_element(OrderPageLocators.DATE_INPUT).click() + self.wait_and_find_element(OrderPageLocators.date_locator(delivery_day)).click() + + @allure.step('Заполняем поле Срок Аренды') + def set_rental_period(self, rental_period): + self.wait_and_find_element(OrderPageLocators.RENTAL_PERIOD_DROPDOWN).click() + self.wait_and_find_element(OrderPageLocators.period_locator(rental_period)).click() + + @allure.step('Кликаем кнопку заказать') + def click_order_button(self): + self.wait_and_find_element(OrderPageLocators.ORDER_BTN).click() + + @allure.step('Подтверждаем заказ') + def click_confirm_button(self): + self.wait_and_find_element(OrderPageLocators.CONFIRM_BTN).click() + + @allure.step('Находим кнопку Статус Заказа') + def find_status_button(self): + return self.wait_and_find_element(OrderPageLocators.STATUS_BTN).is_displayed() + + @allure.step('Заказываем самокат') + def order_scooter(self, name, second_name, address, metro_station, phone_number, day_number, period): + self.set_name(name) + self.set_second_name(second_name) + self.set_address(address) + self.set_metro_station(metro_station) + self.set_phone_number(phone_number) + self.click_continue_button() + self.set_delivery_date(day_number) + self.set_rental_period(period) + self.click_order_button() + self.click_confirm_button() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed9fb5c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +allure-pytest==2.15.0 +allure-python-commons==2.15.0 +attrs==25.3.0 +certifi==2025.10.5 +cffi==2.0.0 +colorama==0.4.6 +Faker==37.11.0 +h11==0.16.0 +idna==3.10 +iniconfig==2.1.0 +outcome==1.3.0.post0 +packaging==25.0 +pluggy==1.6.0 +pycparser==2.23 +Pygments==2.19.2 +PySocks==1.7.1 +pytest==8.4.2 +selenium==4.36.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +trio==0.31.0 +trio-websocket==0.12.2 +typing_extensions==4.15.0 +tzdata==2025.2 +urllib3==2.5.0 +websocket-client==1.8.0 +wsproto==1.2.0 \ No newline at end of file diff --git a/tests/test_faq.py b/tests/test_faq.py new file mode 100644 index 0000000..c961acb --- /dev/null +++ b/tests/test_faq.py @@ -0,0 +1,23 @@ +import allure +import pytest + +from data import FAQData +from pages.main_page import MainPage +from urls import Urls + +@allure.feature('FAQ') +class TestFAQ: + + @allure.title('Проверка ответа на вопрос #{number}') + @allure.description('Проверяем соответствие ответа на выбранный вопрос') + @pytest.mark.parametrize(FAQData.param, FAQData.value) + def test_question_and_answer(self, driver, number, expected_answer): + main_page = MainPage(driver) + main_page.open_page(Urls.MAIN_PAGE) + question_text, answer_text = main_page.get_question_and_answer(number, number) + assert answer_text == expected_answer, ( + f"Неверный ответ для вопроса #{number}\n" + f"Вопрос: {question_text}\n" + f"Ожидаемый ответ: {expected_answer}\n" + f"Фактический ответ: {answer_text}" + ) \ No newline at end of file diff --git a/tests/test_order.py b/tests/test_order.py new file mode 100644 index 0000000..844a6a8 --- /dev/null +++ b/tests/test_order.py @@ -0,0 +1,100 @@ +# tests/test_order.py +import allure +import pytest + +from data import OrderData +from pages.main_page import MainPage +from pages.order_page import OrderPage +from urls import Urls +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +@allure.feature("Заказы") +class TestOrder: + + @allure.title("Проверка редиректа на страницу заказа (верх / низ)") + @allure.description("Проверка редиректа на страницу заказа по верхней и нижней кнопке") + @pytest.mark.parametrize( + "order_button_method, error_message", + [ + pytest.param( + "click_top_order_btn", + "Верхняя кнопка заказа ведёт на неправильную страницу", + id="top_button", + ), + pytest.param( + "click_bottom_order_btn", + "Нижняя кнопка заказа ведёт на неправильную страницу", + id="bottom_button", + ), + ], + ) + def test_order_button_redirect(self, driver, order_button_method, error_message): + page = MainPage(driver) + + page.open_page(Urls.MAIN_PAGE) + getattr(page, order_button_method)() + + assert page.get_current_url() == Urls.ORDER_PAGE, error_message + + @allure.title("Полная процедура заказа самоката (верх / низ)") + @pytest.mark.parametrize( + "order_button_method", + [ + pytest.param("click_top_order_btn", id="top_button"), + pytest.param("click_bottom_order_btn", id="bottom_button"), + ], + ) + @pytest.mark.parametrize(OrderData.param, OrderData.value) + def test_order_scooter_flow( + self, + driver, + order_button_method, + first_name, + last_name, + address, + metro_station, + phone, + delivery_date, + rental_period, + ): + main_page = MainPage(driver) + order_page = OrderPage(driver) + + main_page.open_page(Urls.MAIN_PAGE) + getattr(main_page, order_button_method)() + + order_page.order_scooter( + first_name, + last_name, + address, + metro_station, + phone, + delivery_date, + rental_period, + ) + + assert order_page.find_status_button(), "Не найдена кнопка статуса заказа" + + @allure.title('Переходим на главную страницу через лого "Самокат"') + def test_scooter_logo_redirect(self, driver): + + page = MainPage(driver) + + page.open_page(Urls.ORDER_PAGE) + page.click_to_scooter() + + assert page.get_current_url() == Urls.MAIN_PAGE, 'Редирект по лого "Самокат" ведёт не на главную страницу' + + @allure.title("Переходим на Яндекс Дзен через лого Яндекса") + def test_yandex_logo_redirect(self, driver): + + page = MainPage(driver) + + page.open_page(Urls.ORDER_PAGE) + page.click_to_yandex_logo() + page.switch_to_window(1) + + WebDriverWait(driver, 10).until(EC.url_contains("dzen.ru")) + assert "dzen.ru" in page.get_current_url() diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..91c952b --- /dev/null +++ b/urls.py @@ -0,0 +1,7 @@ +class Urls: + BASE_URL = 'https://qa-scooter.praktikum-services.ru' + + MAIN_PAGE = f'{BASE_URL}/' + ORDER_PAGE = f'{BASE_URL}/order' + DZEN_URL = 'https://dzen.ru/?yredirect=true' +