From c51f02e32b687d643f51bbfd38c66dbe78feeee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:06 +0700 Subject: [PATCH 01/54] chore: add dependencies to the project --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..43bbed5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pytest +selenium +requests +allure-pytest From f49a40429341ab5cff9d39ee2270b61a88527182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:12 +0700 Subject: [PATCH 02/54] feat: add test data and api helpers --- data.py | 4 +++ helpers.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ urls.py | 15 ++++++++++ 3 files changed, 102 insertions(+) create mode 100644 data.py create mode 100644 helpers.py create mode 100644 urls.py diff --git a/data.py b/data.py new file mode 100644 index 0000000..e8a7cf6 --- /dev/null +++ b/data.py @@ -0,0 +1,4 @@ +TEST_EMAIL = "test@example.com" + +INGREDIENT_BUN = "Флюоресцентная булка R2-D3" +INGREDIENT_FILLING = "Соус Spicy-X" diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..25ed5cf --- /dev/null +++ b/helpers.py @@ -0,0 +1,83 @@ +import random +import string + +import allure +import requests +from selenium import webdriver +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.firefox.options import Options as FirefoxOptions + +from urls import ( + INGREDIENTS_ENDPOINT, + ORDERS_ENDPOINT, + REGISTER_ENDPOINT, + USER_ENDPOINT, +) + +FIREFOX_BINARY = "/snap/firefox/current/usr/lib/firefox/firefox" + + +def generate_random_string(length): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for _ in range(length)) + + +def build_random_user_payload(): + return { + "email": f"{generate_random_string(10)}@example.com", + "password": generate_random_string(10), + "name": generate_random_string(10), + } + + +def register_new_user_and_return_data(): + payload = build_random_user_payload() + response = requests.post(REGISTER_ENDPOINT, json=payload) + body = response.json() + return { + "email": payload["email"], + "password": payload["password"], + "name": payload["name"], + "accessToken": body["accessToken"], + "refreshToken": body["refreshToken"], + } + + +@allure.step("Удаляем пользователя") +def delete_user(access_token): + return requests.delete(USER_ENDPOINT, headers={"Authorization": access_token}) + + +def get_ingredient_ids(): + response = requests.get(INGREDIENTS_ENDPOINT) + ingredients = response.json()["data"] + bun_id = next(item["_id"] for item in ingredients if item["type"] == "bun") + filling_id = next(item["_id"] for item in ingredients if item["type"] != "bun") + return bun_id, filling_id + + +@allure.step("Создаём заказ через API") +def create_order_via_api(access_token, ingredients=None): + if ingredients is None: + bun_id, filling_id = get_ingredient_ids() + ingredients = [bun_id, filling_id, bun_id] + response = requests.post( + ORDERS_ENDPOINT, + json={"ingredients": ingredients}, + headers={"Authorization": access_token}, + ) + return response.json() + + +class WebDriverFactory: + @staticmethod + def get_driver(browser_name): + if browser_name == "chrome": + options = ChromeOptions() + options.page_load_strategy = "eager" + return webdriver.Chrome(options=options) + if browser_name == "firefox": + options = FirefoxOptions() + options.binary_location = FIREFOX_BINARY + return webdriver.Firefox(options=options) + raise ValueError(f"Браузер {browser_name} не поддерживается") diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..8039989 --- /dev/null +++ b/urls.py @@ -0,0 +1,15 @@ +BASE_URL = "https://stellarburgers.education-services.ru" + +REGISTER_ENDPOINT = f"{BASE_URL}/api/auth/register" +LOGIN_ENDPOINT = f"{BASE_URL}/api/auth/login" +USER_ENDPOINT = f"{BASE_URL}/api/auth/user" +ORDERS_ENDPOINT = f"{BASE_URL}/api/orders" +INGREDIENTS_ENDPOINT = f"{BASE_URL}/api/ingredients" + +CONSTRUCTOR_URL = f"{BASE_URL}/" +FEED_URL = f"{BASE_URL}/feed" +LOGIN_URL = f"{BASE_URL}/login" +FORGOT_PASSWORD_URL = f"{BASE_URL}/forgot-password" +RESET_PASSWORD_URL = f"{BASE_URL}/reset-password" +ACCOUNT_URL = f"{BASE_URL}/account" +ACCOUNT_ORDERS_URL = f"{BASE_URL}/account/orders" From dc1ab9aacec486c6134bc9bdd591aaae72ccf37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:12 +0700 Subject: [PATCH 03/54] feat(locators): add page element locators --- locators/account_page_locators.py | 13 ++++++++++++ locators/feed_page_locators.py | 24 +++++++++++++++++++++++ locators/forgot_password_page_locators.py | 6 ++++++ locators/header_locators.py | 7 +++++++ locators/login_page_locators.py | 8 ++++++++ locators/main_page_locators.py | 23 ++++++++++++++++++++++ locators/modal_locators.py | 9 +++++++++ locators/reset_password_page_locators.py | 9 +++++++++ 8 files changed, 99 insertions(+) create mode 100644 locators/account_page_locators.py create mode 100644 locators/feed_page_locators.py create mode 100644 locators/forgot_password_page_locators.py create mode 100644 locators/header_locators.py create mode 100644 locators/login_page_locators.py create mode 100644 locators/main_page_locators.py create mode 100644 locators/modal_locators.py create mode 100644 locators/reset_password_page_locators.py diff --git a/locators/account_page_locators.py b/locators/account_page_locators.py new file mode 100644 index 0000000..263bf33 --- /dev/null +++ b/locators/account_page_locators.py @@ -0,0 +1,13 @@ +from selenium.webdriver.common.by import By + +ORDER_HISTORY_LINK = (By.XPATH, ".//a[contains(@href, '/account/orders')]") +LOGOUT_BUTTON = (By.XPATH, ".//button[contains(., 'Выход')]") +ORDER_HISTORY_LINKS = (By.XPATH, ".//a[contains(@href, '/account/orders/')]") + + +def order_number_in_history(order_number): + formatted_number = f"{order_number:07d}" + return ( + By.XPATH, + f".//a[contains(@href, '/account/orders/') and contains(., '#{formatted_number}')]", + ) diff --git a/locators/feed_page_locators.py b/locators/feed_page_locators.py new file mode 100644 index 0000000..d275610 --- /dev/null +++ b/locators/feed_page_locators.py @@ -0,0 +1,24 @@ +from selenium.webdriver.common.by import By + +FEED_TITLE = (By.XPATH, ".//h1[contains(text(), 'Лента заказов')]") +TOTAL_DONE_COUNTER = (By.XPATH, ".//p[contains(text(), 'Выполнено за все время')]/following-sibling::p") +TODAY_DONE_COUNTER = (By.XPATH, ".//p[contains(text(), 'Выполнено за сегодня')]/following-sibling::p") +IN_PROGRESS_SECTION = (By.XPATH, ".//p[contains(text(), 'В работе')]") +FEED_ORDER_LINKS = (By.XPATH, ".//a[contains(@href, '/feed/')]") +ORDER_MODAL = (By.XPATH, ".//section[contains(@class, 'Modal_modal_opened')]") + + +def feed_order_link(order_number): + formatted_number = f"{order_number:07d}" + return ( + By.XPATH, + f".//a[contains(@href, '/feed/') and contains(., '#{formatted_number}')]", + ) + + +def order_in_progress(order_number): + formatted_number = f"{order_number:07d}" + return ( + By.XPATH, + f".//p[contains(text(), 'В работе')]/following-sibling::*[contains(text(), '{formatted_number}')]", + ) diff --git a/locators/forgot_password_page_locators.py b/locators/forgot_password_page_locators.py new file mode 100644 index 0000000..2986e6f --- /dev/null +++ b/locators/forgot_password_page_locators.py @@ -0,0 +1,6 @@ +from selenium.webdriver.common.by import By + +PAGE_TITLE = (By.XPATH, ".//h2[contains(text(), 'Восстановление пароля')]") +EMAIL_INPUT = (By.XPATH, ".//input[@name='name']") +RESTORE_BUTTON = (By.XPATH, ".//button[text()='Восстановить']") +LOGIN_LINK = (By.XPATH, ".//a[contains(@href, '/login')]") diff --git a/locators/header_locators.py b/locators/header_locators.py new file mode 100644 index 0000000..ea94f92 --- /dev/null +++ b/locators/header_locators.py @@ -0,0 +1,7 @@ +from selenium.webdriver.common.by import By + +CONSTRUCTOR_LINK = (By.XPATH, ".//a[normalize-space()='Конструктор']") +FEED_LINK = (By.XPATH, ".//a[contains(normalize-space(), 'Лента')]") +ACCOUNT_LINK = (By.XPATH, ".//a[contains(@href, '/account')]") +LOGIN_BUTTON = (By.XPATH, ".//a[contains(@href, '/login')]") +PERSONAL_ACCOUNT_LINK = (By.XPATH, ".//a[contains(normalize-space(), 'Личный')]") diff --git a/locators/login_page_locators.py b/locators/login_page_locators.py new file mode 100644 index 0000000..5daa620 --- /dev/null +++ b/locators/login_page_locators.py @@ -0,0 +1,8 @@ +from selenium.webdriver.common.by import By + +PAGE_TITLE = (By.XPATH, ".//h2[text()='Вход']") +EMAIL_INPUT = (By.XPATH, ".//input[@name='name']") +PASSWORD_INPUT = (By.XPATH, ".//input[@type='password']") +LOGIN_BUTTON = (By.XPATH, ".//button[text()='Войти']") +FORGOT_PASSWORD_LINK = (By.XPATH, ".//a[contains(@href, '/forgot-password')]") +REGISTER_LINK = (By.XPATH, ".//a[contains(@href, '/register')]") diff --git a/locators/main_page_locators.py b/locators/main_page_locators.py new file mode 100644 index 0000000..b7ba32c --- /dev/null +++ b/locators/main_page_locators.py @@ -0,0 +1,23 @@ +from selenium.webdriver.common.by import By + +CONSTRUCTOR_TITLE = (By.XPATH, ".//h1[text()='Соберите бургер']") +BUN_TAB = (By.XPATH, ".//span[text()='Булки']/parent::div") +SAUCES_TAB = (By.XPATH, ".//span[text()='Соусы']/parent::div") +FILLINGS_TAB = (By.XPATH, ".//span[text()='Начинки']/parent::div") +CONSTRUCTOR_TOP_DROP_ZONE = (By.XPATH, ".//div[contains(@class, 'constructor-element_pos_top')]") +CONSTRUCTOR_BOTTOM_DROP_ZONE = (By.XPATH, ".//div[contains(@class, 'constructor-element_pos_bottom')]") +CONSTRUCTOR_BASKET = (By.XPATH, ".//div[contains(@class, 'BurgerConstructor_basket__container')]") +PLACE_ORDER_BUTTON = (By.XPATH, ".//button[contains(text(), 'Оформить заказ')]") +INGREDIENT_MODAL = (By.XPATH, ".//section[contains(@class, 'Modal_modal_opened')]") + + +def ingredient_link_by_name(name): + return (By.XPATH, f".//a[contains(@href, '/ingredient/') and .//p[contains(text(), '{name}')]]") + + +def ingredient_counter_by_name(name): + return ( + By.XPATH, + f".//a[contains(@href, '/ingredient/') and .//p[contains(text(), '{name}')]]" + f"//div[contains(@class, 'counter')]", + ) diff --git a/locators/modal_locators.py b/locators/modal_locators.py new file mode 100644 index 0000000..dba3ba7 --- /dev/null +++ b/locators/modal_locators.py @@ -0,0 +1,9 @@ +from selenium.webdriver.common.by import By + +MODAL_OPENED = (By.XPATH, ".//section[contains(@class, 'Modal_modal_opened')]") +MODAL_CLOSE_BUTTON = (By.XPATH, ".//button[contains(@class, 'modal__close')]") +ORDER_SUCCESS_MODAL = ( + By.XPATH, + ".//h2[contains(text(), 'идентификатор заказа')]/ancestor::section[contains(@class, 'Modal')]", +) +ORDER_NUMBER_TEXT = (By.XPATH, ".//h2[contains(@class, 'digits')]") diff --git a/locators/reset_password_page_locators.py b/locators/reset_password_page_locators.py new file mode 100644 index 0000000..b19613e --- /dev/null +++ b/locators/reset_password_page_locators.py @@ -0,0 +1,9 @@ +from selenium.webdriver.common.by import By + +PAGE_TITLE = (By.XPATH, ".//h2[contains(text(), 'Восстановление пароля')]") +PASSWORD_INPUT = (By.XPATH, ".//input[@type='password' or @name='password']") +PASSWORD_VISIBILITY_TOGGLE = ( + By.XPATH, + ".//input[@type='password' or @name='password']" + "/ancestor::div[contains(@class, 'input')]//div[contains(@class, 'icon')]", +) From cafc38ae1944950e7db4d7fe558cbd4684e2d9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:12 +0700 Subject: [PATCH 04/54] feat(pages): add page object model classes --- pages/account_page.py | 22 +++++++ pages/base_page.py | 115 ++++++++++++++++++++++++++++++++++ pages/feed_page.py | 58 +++++++++++++++++ pages/forgot_password_page.py | 28 +++++++++ pages/login_page.py | 29 +++++++++ pages/main_page.py | 78 +++++++++++++++++++++++ pages/reset_password_page.py | 25 ++++++++ 7 files changed, 355 insertions(+) create mode 100644 pages/account_page.py create mode 100644 pages/base_page.py create mode 100644 pages/feed_page.py create mode 100644 pages/forgot_password_page.py create mode 100644 pages/login_page.py create mode 100644 pages/main_page.py create mode 100644 pages/reset_password_page.py diff --git a/pages/account_page.py b/pages/account_page.py new file mode 100644 index 0000000..c03fb29 --- /dev/null +++ b/pages/account_page.py @@ -0,0 +1,22 @@ +import re + +import allure + +from locators import account_page_locators +from pages.base_page import BasePage +from urls import ACCOUNT_ORDERS_URL, ACCOUNT_URL + + +class AccountPage(BasePage): + @allure.step("Переходим в историю заказов") + def go_to_order_history(self): + self.click(account_page_locators.ORDER_HISTORY_LINK) + self.wait_for_url_contains("/account/orders") + + @allure.step("Выходим из аккаунта") + def logout(self): + self.click(account_page_locators.LOGOUT_BUTTON) + self.wait_for_url_contains("/login") + + def is_order_in_history(self, order_number): + return bool(self.find_elements(account_page_locators.order_number_in_history(order_number))) diff --git a/pages/base_page.py b/pages/base_page.py new file mode 100644 index 0000000..249ddfd --- /dev/null +++ b/pages/base_page.py @@ -0,0 +1,115 @@ +import allure +from selenium.common.exceptions import ElementClickInterceptedException +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.wait import WebDriverWait + +from locators import header_locators + + +class BasePage: + DEFAULT_TIMEOUT = 5 + + def __init__(self, driver): + self.driver = driver + + @property + def current_url(self): + return self.driver.current_url + + def wait_for_presence(self, locator, timeout=DEFAULT_TIMEOUT): + return WebDriverWait(self.driver, timeout).until(expected_conditions.presence_of_element_located(locator)) + + def wait_for_visibility(self, locator, timeout=DEFAULT_TIMEOUT): + return WebDriverWait(self.driver, timeout).until(expected_conditions.visibility_of_element_located(locator)) + + def wait_for_clickable(self, locator, timeout=DEFAULT_TIMEOUT): + return WebDriverWait(self.driver, timeout).until(expected_conditions.element_to_be_clickable(locator)) + + def wait_for_url_contains(self, url_part, timeout=DEFAULT_TIMEOUT): + return WebDriverWait(self.driver, timeout).until(expected_conditions.url_contains(url_part)) + + def wait_until_invisible(self, locator, timeout=DEFAULT_TIMEOUT): + return WebDriverWait(self.driver, timeout).until(expected_conditions.invisibility_of_element_located(locator)) + + def find_element(self, locator): + return self.driver.find_element(*locator) + + def find_elements(self, locator): + return self.driver.find_elements(*locator) + + def click(self, locator, timeout=DEFAULT_TIMEOUT): + element = self.wait_for_clickable(locator, timeout) + try: + element.click() + except ElementClickInterceptedException: + self.close_modal_if_present() + element = self.wait_for_clickable(locator, timeout) + self.driver.execute_script("arguments[0].click();", element) + + def send_keys(self, locator, text, timeout=DEFAULT_TIMEOUT): + element = self.wait_for_visibility(locator, timeout) + element.clear() + element.send_keys(text) + + def get_text(self, locator, timeout=DEFAULT_TIMEOUT): + return self.wait_for_visibility(locator, timeout).text + + def is_displayed(self, locator, timeout=DEFAULT_TIMEOUT): + return self.wait_for_visibility(locator, timeout).is_displayed() + + def drag_and_drop(self, source_locator, target_locator, timeout=DEFAULT_TIMEOUT): + source = self.wait_for_visibility(source_locator, timeout) + target = self.wait_for_visibility(target_locator, timeout) + self.driver.execute_script( + """ + const source = arguments[0]; + const target = arguments[1]; + const dataTransfer = { + data: {}, + setData(key, value) { + this.data[key] = value; + }, + getData(key) { + return this.data[key]; + }, + }; + ["dragstart", "dragenter", "dragover", "drop", "dragend"].forEach((type, index) => { + const element = index === 0 || index === 4 ? source : target; + const event = new Event(type, { bubbles: true, cancelable: true }); + event.dataTransfer = dataTransfer; + element.dispatchEvent(event); + }); + """, + source, + target, + ) + + @allure.step("Закрываем модальное окно, если оно открыто") + def close_modal_if_present(self): + close_buttons = self.driver.find_elements(By.XPATH, ".//button[contains(@class, 'modal__close')]") + for button in close_buttons: + if button.is_displayed(): + button.click() + return + + @allure.step("Переходим в конструктор") + def go_to_constructor(self): + self.click(header_locators.CONSTRUCTOR_LINK) + + @allure.step("Переходим в ленту заказов") + def go_to_feed(self): + self.click(header_locators.FEED_LINK) + + @allure.step("Переходим в личный кабинет") + def go_to_personal_account(self): + self.click(header_locators.PERSONAL_ACCOUNT_LINK) + + @allure.step("Авторизуем пользователя через localStorage") + def authorize_by_token(self, access_token): + self.driver.execute_script( + 'localStorage.setItem("accessToken", arguments[0]);', + access_token, + ) + self.driver.refresh() + self.close_modal_if_present() diff --git a/pages/feed_page.py b/pages/feed_page.py new file mode 100644 index 0000000..5a39df1 --- /dev/null +++ b/pages/feed_page.py @@ -0,0 +1,58 @@ +import re + +import allure +from selenium.webdriver.support.wait import WebDriverWait + +from locators import feed_page_locators +from pages.base_page import BasePage +from urls import FEED_URL + + +class FeedPage(BasePage): + @allure.step("Открываем ленту заказов") + def open(self): + self.driver.get(FEED_URL) + self.close_modal_if_present() + self.wait_for_visibility(feed_page_locators.FEED_TITLE) + + @allure.step("Открываем заказ #{order_number} в ленте") + def click_order(self, order_number): + self.click(feed_page_locators.feed_order_link(order_number)) + + @allure.step("Получаем значение «Выполнено за всё время»") + def get_total_done_count(self): + return self._parse_counter(feed_page_locators.TOTAL_DONE_COUNTER) + + @allure.step("Получаем значение «Выполнено за сегодня»") + def get_today_done_count(self): + return self._parse_counter(feed_page_locators.TODAY_DONE_COUNTER) + + @allure.step("Проверяем, что заказ #{order_number} в работе") + def is_order_in_progress(self, order_number): + return bool(self.find_elements(feed_page_locators.order_in_progress(order_number))) + + @allure.step("Получаем номера заказов из ленты") + def get_order_numbers_from_feed(self): + numbers = [] + for link in self.find_elements(feed_page_locators.FEED_ORDER_LINKS): + match = re.search(r"#(\d+)", link.text) + if match: + numbers.append(int(match.group(1))) + return numbers + + @allure.step("Проверяем, что модалка заказа открыта") + def is_order_modal_visible(self): + return self.is_displayed(feed_page_locators.ORDER_MODAL) + + @allure.step("Ждём увеличения счётчика") + def wait_for_counter_increase(self, getter, previous_value, timeout=20): + def counter_increased(_driver): + self.driver.refresh() + self.wait_for_visibility(feed_page_locators.FEED_TITLE, timeout=5) + return getter() > previous_value + + WebDriverWait(self.driver, timeout, poll_frequency=1).until(counter_increased) + + def _parse_counter(self, locator): + text = self.get_text(locator) + return int(re.sub(r"\D", "", text)) diff --git a/pages/forgot_password_page.py b/pages/forgot_password_page.py new file mode 100644 index 0000000..1893646 --- /dev/null +++ b/pages/forgot_password_page.py @@ -0,0 +1,28 @@ +import allure + +from locators import forgot_password_page_locators +from pages.base_page import BasePage +from urls import FORGOT_PASSWORD_URL + + +class ForgotPasswordPage(BasePage): + @allure.step("Открываем страницу восстановления пароля") + def open(self): + self.driver.get(FORGOT_PASSWORD_URL) + self.close_modal_if_present() + self.wait_for_visibility(forgot_password_page_locators.PAGE_TITLE) + + @allure.step("Вводим email: {email}") + def enter_email(self, email): + self.send_keys(forgot_password_page_locators.EMAIL_INPUT, email) + + @allure.step("Нажимаем кнопку «Восстановить»") + def click_restore(self): + self.click(forgot_password_page_locators.RESTORE_BUTTON) + self.wait_for_url_contains("/reset-password") + + def is_open(self): + return "/forgot-password" in self.current_url + + def is_reset_page_open(self): + return "/reset-password" in self.current_url diff --git a/pages/login_page.py b/pages/login_page.py new file mode 100644 index 0000000..d923fa0 --- /dev/null +++ b/pages/login_page.py @@ -0,0 +1,29 @@ +import allure +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.wait import WebDriverWait + +from locators import login_page_locators +from pages.base_page import BasePage +from urls import LOGIN_URL + + +class LoginPage(BasePage): + @allure.step("Открываем страницу входа") + def open(self): + self.driver.get(LOGIN_URL) + self.close_modal_if_present() + self.wait_for_visibility(login_page_locators.PAGE_TITLE) + + @allure.step("Переходим на страницу восстановления пароля") + def click_forgot_password(self): + self.click(login_page_locators.FORGOT_PASSWORD_LINK) + + @allure.step("Авторизуемся: email={email}") + def login(self, email, password): + self.send_keys(login_page_locators.EMAIL_INPUT, email) + self.send_keys(login_page_locators.PASSWORD_INPUT, password) + self.click(login_page_locators.LOGIN_BUTTON) + WebDriverWait(self.driver, 10).until(lambda driver: "/login" not in driver.current_url) + + def is_open(self): + return "/login" in self.current_url diff --git a/pages/main_page.py b/pages/main_page.py new file mode 100644 index 0000000..8894377 --- /dev/null +++ b/pages/main_page.py @@ -0,0 +1,78 @@ +import allure + +from locators import main_page_locators, modal_locators +from pages.base_page import BasePage +from urls import CONSTRUCTOR_URL + + +class MainPage(BasePage): + @allure.step("Открываем конструктор") + def open(self): + self.driver.get(CONSTRUCTOR_URL) + self.close_modal_if_present() + self.wait_for_visibility(main_page_locators.CONSTRUCTOR_TITLE) + + @allure.step("Открываем вкладку «Соусы»") + def open_sauces_tab(self): + self.click(main_page_locators.SAUCES_TAB) + + @allure.step("Открываем ингредиент: {name}") + def click_ingredient(self, name): + self.click(main_page_locators.ingredient_link_by_name(name)) + self.wait_for_visibility(main_page_locators.INGREDIENT_MODAL) + + @allure.step("Получаем счётчик ингредиента: {name}") + def get_ingredient_counter(self, name): + counter = self.wait_for_visibility(main_page_locators.ingredient_counter_by_name(name)) + return int(counter.text) + + @allure.step("Добавляем булку в заказ: {name}") + def add_bun_to_order(self, name, position="top"): + target = ( + main_page_locators.CONSTRUCTOR_TOP_DROP_ZONE + if position == "top" + else main_page_locators.CONSTRUCTOR_BOTTOM_DROP_ZONE + ) + self.drag_and_drop(main_page_locators.ingredient_link_by_name(name), target) + + @allure.step("Добавляем начинку или соус в заказ: {name}") + def add_filling_to_order(self, name): + if "Соус" in name: + self.open_sauces_tab() + self.drag_and_drop( + main_page_locators.ingredient_link_by_name(name), + main_page_locators.CONSTRUCTOR_BASKET, + ) + + @allure.step("Собираем бургер по умолчанию") + def build_default_burger(self, bun_name, filling_name): + self.add_bun_to_order(bun_name, position="top") + self.add_filling_to_order(filling_name) + self.add_bun_to_order(bun_name, position="bottom") + self.wait_for_visibility(main_page_locators.PLACE_ORDER_BUTTON, timeout=10) + + @allure.step("Нажимаем «Оформить заказ»") + def click_place_order(self): + self.click(main_page_locators.PLACE_ORDER_BUTTON) + + @allure.step("Проверяем, что модалка ингредиента открыта") + def is_ingredient_modal_visible(self): + return self.is_displayed(main_page_locators.INGREDIENT_MODAL) + + @allure.step("Закрываем модалку") + def close_modal(self): + self.click(modal_locators.MODAL_CLOSE_BUTTON) + self.wait_until_invisible(main_page_locators.INGREDIENT_MODAL) + + @allure.step("Проверяем, что модалка закрыта") + def is_modal_closed(self): + return len(self.find_elements(main_page_locators.INGREDIENT_MODAL)) == 0 + + @allure.step("Проверяем, что заказ оформлен") + def is_order_success_modal_visible(self): + return self.is_displayed(modal_locators.ORDER_SUCCESS_MODAL) + + @allure.step("Получаем номер оформленного заказа") + def get_order_number_from_modal(self): + heading = self.wait_for_visibility(modal_locators.ORDER_NUMBER_TEXT) + return int(heading.text.strip()) diff --git a/pages/reset_password_page.py b/pages/reset_password_page.py new file mode 100644 index 0000000..87e5ab6 --- /dev/null +++ b/pages/reset_password_page.py @@ -0,0 +1,25 @@ +import allure +from selenium.webdriver.common.by import By + +from locators import reset_password_page_locators +from pages.base_page import BasePage +from urls import RESET_PASSWORD_URL + + +class ResetPasswordPage(BasePage): + @allure.step("Открываем страницу сброса пароля") + def open(self): + self.driver.get(RESET_PASSWORD_URL) + self.wait_for_visibility(reset_password_page_locators.PAGE_TITLE) + + @allure.step("Нажимаем кнопку показать/скрыть пароль") + def click_password_visibility_toggle(self): + self.wait_for_visibility(reset_password_page_locators.PASSWORD_INPUT) + self.click(reset_password_page_locators.PASSWORD_VISIBILITY_TOGGLE) + + @allure.step("Проверяем, что поле пароля подсвечено") + def is_password_field_active(self): + password_input = self.wait_for_visibility(reset_password_page_locators.PASSWORD_INPUT) + label = password_input.find_element(By.XPATH, "./ancestor::div[contains(@class, 'input')]//label") + class_name = label.get_attribute("class") or "" + return "input__placeholder-focused" in class_name From cd3df35ed1bdee7a5f86b962c1268b5ce713666a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:12 +0700 Subject: [PATCH 05/54] test: add shared pytest fixtures --- conftest.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..d38e1ef --- /dev/null +++ b/conftest.py @@ -0,0 +1,31 @@ +import allure +import pytest + +from helpers import WebDriverFactory, delete_user, register_new_user_and_return_data +from pages.base_page import BasePage + + +@pytest.fixture(autouse=True) +def reset_browser_state(driver): + driver.execute_script("localStorage.clear(); sessionStorage.clear();") + driver.delete_all_cookies() + yield + BasePage(driver).close_modal_if_present() + + +@pytest.fixture(params=["chrome", "firefox"], scope="class") +def driver(request): + with allure.step(f"Открываем браузер {request.param}"): + browser = WebDriverFactory.get_driver(request.param) + yield browser + with allure.step("Закрываем браузер"): + browser.quit() + + +@pytest.fixture +def user(): + with allure.step("Создаём пользователя для теста"): + user_data = register_new_user_and_return_data() + yield user_data + with allure.step("Удаляем пользователя после теста"): + delete_user(user_data["accessToken"]) From 2fabb70190808a887992856f94f84e2bc8bcf71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:20 +0700 Subject: [PATCH 06/54] test(password-recovery): add ui tests for password recovery --- tests/test_password_recovery.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_password_recovery.py diff --git a/tests/test_password_recovery.py b/tests/test_password_recovery.py new file mode 100644 index 0000000..9da4114 --- /dev/null +++ b/tests/test_password_recovery.py @@ -0,0 +1,42 @@ +import allure + +from data import TEST_EMAIL +from pages.forgot_password_page import ForgotPasswordPage +from pages.login_page import LoginPage +from pages.reset_password_page import ResetPasswordPage +from urls import BASE_URL, FORGOT_PASSWORD_URL + + +@allure.link(BASE_URL, name="Stellar Burgers") +class TestPasswordRecovery: + @allure.title("Переход на страницу восстановления пароля") + @allure.description("По кнопке «Восстановить пароль» открывается страница /forgot-password.") + def test_navigate_to_forgot_password_page(self, driver): + login_page = LoginPage(driver) + login_page.open() + login_page.click_forgot_password() + + assert FORGOT_PASSWORD_URL in driver.current_url + + @allure.title("Восстановление пароля по email") + @allure.description("После ввода email и клика «Восстановить» открывается страница сброса пароля.") + def test_restore_password_with_email(self, driver): + forgot_password_page = ForgotPasswordPage(driver) + forgot_password_page.open() + forgot_password_page.enter_email(TEST_EMAIL) + forgot_password_page.click_restore() + + assert forgot_password_page.is_reset_page_open() + + @allure.title("Переключатель видимости пароля подсвечивает поле") + @allure.description("После восстановления пароля клик по иконке подсвечивает поле нового пароля.") + def test_password_visibility_toggle_highlights_field(self, driver): + forgot_password_page = ForgotPasswordPage(driver) + forgot_password_page.open() + forgot_password_page.enter_email(TEST_EMAIL) + forgot_password_page.click_restore() + + reset_password_page = ResetPasswordPage(driver) + reset_password_page.click_password_visibility_toggle() + + assert reset_password_page.is_password_field_active() From e85227ba8c261a90a8dceaa2236ae669f93882e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:20 +0700 Subject: [PATCH 07/54] test(personal-account): add ui tests for personal account --- tests/test_personal_account.py | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/test_personal_account.py diff --git a/tests/test_personal_account.py b/tests/test_personal_account.py new file mode 100644 index 0000000..ee724bd --- /dev/null +++ b/tests/test_personal_account.py @@ -0,0 +1,44 @@ +import allure + +from pages.account_page import AccountPage +from pages.login_page import LoginPage +from urls import ACCOUNT_ORDERS_URL, ACCOUNT_URL, BASE_URL, LOGIN_URL + + +@allure.link(BASE_URL, name="Stellar Burgers") +class TestPersonalAccount: + @allure.title("Переход в личный кабинет") + @allure.description("После авторизации клик по «Личный кабинет» открывает /account.") + def test_navigate_to_account(self, driver, user): + login_page = LoginPage(driver) + login_page.open() + login_page.login(user["email"], user["password"]) + login_page.go_to_personal_account() + + assert ACCOUNT_URL in driver.current_url + + @allure.title("Переход в историю заказов") + @allure.description("Из личного кабинета можно перейти в раздел «История заказов».") + def test_navigate_to_order_history(self, driver, user): + login_page = LoginPage(driver) + login_page.open() + login_page.login(user["email"], user["password"]) + login_page.go_to_personal_account() + + account_page = AccountPage(driver) + account_page.go_to_order_history() + + assert ACCOUNT_ORDERS_URL in driver.current_url + + @allure.title("Выход из аккаунта") + @allure.description("После выхода пользователь перенаправляется на страницу входа.") + def test_logout(self, driver, user): + login_page = LoginPage(driver) + login_page.open() + login_page.login(user["email"], user["password"]) + login_page.go_to_personal_account() + + account_page = AccountPage(driver) + account_page.logout() + + assert LOGIN_URL in driver.current_url From b1b3f80cd6fa452b5b5ff4bffb262d4b24e3545a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:20 +0700 Subject: [PATCH 08/54] test(main): add ui tests for main functionality --- tests/test_main_functionality.py | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_main_functionality.py diff --git a/tests/test_main_functionality.py b/tests/test_main_functionality.py new file mode 100644 index 0000000..e9c5ad6 --- /dev/null +++ b/tests/test_main_functionality.py @@ -0,0 +1,68 @@ +import allure + +from data import INGREDIENT_BUN, INGREDIENT_FILLING +from pages.feed_page import FeedPage +from pages.main_page import MainPage +from urls import BASE_URL, CONSTRUCTOR_URL, FEED_URL + + +@allure.link(BASE_URL, name="Stellar Burgers") +class TestMainFunctionality: + @allure.title("Переход в конструктор") + @allure.description("По клику «Конструктор» открывается главная страница.") + def test_navigate_to_constructor(self, driver): + feed_page = FeedPage(driver) + feed_page.open() + feed_page.go_to_constructor() + + assert CONSTRUCTOR_URL in driver.current_url + + @allure.title("Переход в ленту заказов") + @allure.description("По клику «Лента заказов» открывается страница /feed.") + def test_navigate_to_feed(self, driver): + main_page = MainPage(driver) + main_page.open() + main_page.go_to_feed() + + assert FEED_URL in driver.current_url + + @allure.title("Открытие модалки ингредиента") + @allure.description("При клике на ингредиент появляется всплывающее окно с деталями.") + def test_ingredient_modal_opens(self, driver): + main_page = MainPage(driver) + main_page.open() + main_page.click_ingredient(INGREDIENT_BUN) + + assert main_page.is_ingredient_modal_visible() + + @allure.title("Закрытие модалки ингредиента") + @allure.description("Модалка закрывается по клику на крестик.") + def test_ingredient_modal_closes(self, driver): + main_page = MainPage(driver) + main_page.open() + main_page.click_ingredient(INGREDIENT_BUN) + main_page.close_modal() + + assert main_page.is_modal_closed() + + @allure.title("Увеличение счётчика ингредиента") + @allure.description("При добавлении ингредиента в заказ его счётчик увеличивается.") + def test_ingredient_counter_increases(self, driver): + main_page = MainPage(driver) + main_page.open() + main_page.open_sauces_tab() + counter_before = main_page.get_ingredient_counter(INGREDIENT_FILLING) + main_page.add_filling_to_order(INGREDIENT_FILLING) + + assert main_page.get_ingredient_counter(INGREDIENT_FILLING) == counter_before + 1 + + @allure.title("Оформление заказа авторизованным пользователем") + @allure.description("Залогиненный пользователь может оформить заказ из конструктора.") + def test_logged_in_user_can_place_order(self, driver, user): + main_page = MainPage(driver) + main_page.open() + main_page.authorize_by_token(user["accessToken"]) + main_page.build_default_burger(INGREDIENT_BUN, INGREDIENT_FILLING) + main_page.click_place_order() + + assert main_page.is_order_success_modal_visible() From b9f123916a8391ec33231f8bf24aef27de12d234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:20 +0700 Subject: [PATCH 09/54] test(order-feed): add ui tests for order feed --- tests/test_order_feed.py | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_order_feed.py diff --git a/tests/test_order_feed.py b/tests/test_order_feed.py new file mode 100644 index 0000000..9596436 --- /dev/null +++ b/tests/test_order_feed.py @@ -0,0 +1,83 @@ +import allure + +from data import INGREDIENT_BUN, INGREDIENT_FILLING +from helpers import create_order_via_api +from pages.account_page import AccountPage +from pages.feed_page import FeedPage +from pages.main_page import MainPage +from urls import ACCOUNT_ORDERS_URL, BASE_URL + + +@allure.link(BASE_URL, name="Stellar Burgers") +class TestOrderFeed: + @allure.title("Открытие модалки заказа из ленты") + @allure.description("При клике на заказ в ленте открывается всплывающее окно с деталями.") + def test_order_modal_opens_from_feed(self, driver, user): + order_data = create_order_via_api(user["accessToken"]) + order_number = order_data["order"]["number"] + + feed_page = FeedPage(driver) + feed_page.open() + feed_page.click_order(order_number) + + assert feed_page.is_order_modal_visible() + + @allure.title("Заказы пользователя отображаются в ленте") + @allure.description("Заказ из истории пользователя виден на странице «Лента заказов».") + def test_user_orders_appear_in_feed(self, driver, user): + order_data = create_order_via_api(user["accessToken"]) + order_number = order_data["order"]["number"] + + main_page = MainPage(driver) + main_page.open() + main_page.authorize_by_token(user["accessToken"]) + driver.get(ACCOUNT_ORDERS_URL) + + account_page = AccountPage(driver) + assert account_page.is_order_in_history(order_number) + + feed_page = FeedPage(driver) + feed_page.open() + + assert order_number in feed_page.get_order_numbers_from_feed() + + @allure.title("Счётчик «Выполнено за всё время» увеличивается") + @allure.description("После создания заказа общий счётчик выполненных заказов растёт.") + def test_total_done_counter_increases(self, driver, user): + feed_page = FeedPage(driver) + feed_page.open() + total_before = feed_page.get_total_done_count() + + create_order_via_api(user["accessToken"]) + feed_page.open() + feed_page.wait_for_counter_increase(feed_page.get_total_done_count, total_before) + + assert feed_page.get_total_done_count() > total_before + + @allure.title("Счётчик «Выполнено за сегодня» увеличивается") + @allure.description("После создания заказа дневной счётчик выполненных заказов растёт.") + def test_today_done_counter_increases(self, driver, user): + feed_page = FeedPage(driver) + feed_page.open() + today_before = feed_page.get_today_done_count() + + create_order_via_api(user["accessToken"]) + feed_page.open() + feed_page.wait_for_counter_increase(feed_page.get_today_done_count, today_before) + + assert feed_page.get_today_done_count() > today_before + + @allure.title("Новый заказ появляется в разделе «В работе»") + @allure.description("После оформления заказа его номер отображается в блоке «В работе».") + def test_new_order_appears_in_progress(self, driver, user): + main_page = MainPage(driver) + main_page.open() + main_page.authorize_by_token(user["accessToken"]) + main_page.build_default_burger(INGREDIENT_BUN, INGREDIENT_FILLING) + main_page.click_place_order() + order_number = main_page.get_order_number_from_modal() + + feed_page = FeedPage(driver) + feed_page.open() + + assert feed_page.is_order_in_progress(order_number) From 01a82ec1021e6738909cb15c8bdfd04dabb122ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D0=B4=D0=B8=20=D0=93=D0=B5=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=93=D0=B5=D0=BD=D0=BD=D0=B0=D0=B4=D1=8C=D0=B5=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 17 Jul 2026 17:23:25 +0700 Subject: [PATCH 10/54] test: add allure test results --- allure_results/37ccb523-9805-4e10-9a10-3ac24267bb73-result.json | 1 + .../7e1b0006-dd51-4861-becb-b1b1229cccaf-container.json | 1 + allure_results/a6da569f-323c-426f-b3cf-89b97ae24ef2-result.json | 1 + allure_results/ad3e8e7f-7ac8-4512-9d03-8258c0981c5b-result.json | 1 + 4 files changed, 4 insertions(+) create mode 100644 allure_results/37ccb523-9805-4e10-9a10-3ac24267bb73-result.json create mode 100644 allure_results/7e1b0006-dd51-4861-becb-b1b1229cccaf-container.json create mode 100644 allure_results/a6da569f-323c-426f-b3cf-89b97ae24ef2-result.json create mode 100644 allure_results/ad3e8e7f-7ac8-4512-9d03-8258c0981c5b-result.json diff --git a/allure_results/37ccb523-9805-4e10-9a10-3ac24267bb73-result.json b/allure_results/37ccb523-9805-4e10-9a10-3ac24267bb73-result.json new file mode 100644 index 0000000..5e3a53a --- /dev/null +++ b/allure_results/37ccb523-9805-4e10-9a10-3ac24267bb73-result.json @@ -0,0 +1 @@ +{"name": "Переключатель видимости пароля подсвечивает поле", "status": "broken", "statusDetails": {"message": "selenium.common.exceptions.TimeoutException: Message: \nStacktrace:\nRemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8\nWebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:169:5\nNoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:538:5\ndom.find/\ndriver = \n\n @allure.title(\"Переключатель видимости пароля подсвечивает поле\")\n @allure.description(\"Клик по иконке показа/скрытия пароля активирует поле ввода.\")\n def test_password_visibility_toggle_highlights_field(self, driver):\n reset_password_page = ResetPasswordPage(driver)\n reset_password_page.open()\n> reset_password_page.click_password_visibility_toggle()\n\ntests/test_password_recovery.py:36: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \npages/reset_password_page.py:16: in click_password_visibility_toggle\n self.click(reset_password_page_locators.PASSWORD_VISIBILITY_TOGGLE)\npages/base_page.py:49: in click\n self.wait_for_clickable(locator, timeout).click()\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\npages/base_page.py:28: in wait_for_clickable\n return WebDriverWait(self.driver, timeout).until(\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = \nmethod = ._predicate at 0x724cc687eac0>\nmessage = ''\n\n def until(self, method: Callable[[D], Literal[False] | T], message: str = \"\") -> T:\n \"\"\"Wait until the method returns a value that is not False.\n \n Calls the method provided with the driver as an argument until the\n return value does not evaluate to ``False``.\n \n Args:\n method: A callable object that takes a WebDriver instance as an\n argument.\n message: Optional message for TimeoutException.\n \n Returns:\n The result of the last call to `method`.\n \n Raises:\n TimeoutException: If 'method' does not return a truthy value within\n the WebDriverWait object's timeout.\n \n Example:\n >>> from selenium.webdriver.common.by import By\n >>> from selenium.webdriver.support.wait import WebDriverWait\n >>> from selenium.webdriver.support import expected_conditions as EC\n >>>\n >>> # Wait until an element is visible on the page\n >>> wait = WebDriverWait(driver, 10)\n >>> element = wait.until(EC.visibility_of_element_located((By.ID, \"exampleId\")))\n >>> print(element.text)\n \"\"\"\n screen = None\n stacktrace = None\n \n end_time = time.monotonic() + self._timeout\n while True:\n try:\n value = method(self._driver)\n if value:\n return value\n except self._ignored_exceptions as exc:\n screen = getattr(exc, \"screen\", None)\n stacktrace = getattr(exc, \"stacktrace\", None)\n if time.monotonic() > end_time:\n break\n time.sleep(self._poll)\n> raise TimeoutException(message, screen, stacktrace)\nE selenium.common.exceptions.TimeoutException: Message: \nE Stacktrace:\nE RemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8\nE WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:169:5\nE NoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:538:5\nE dom.find/", "start": 1784278043891}], "start": 1784277968442, "stop": 1784278043892} \ No newline at end of file diff --git a/allure_results/a6da569f-323c-426f-b3cf-89b97ae24ef2-result.json b/allure_results/a6da569f-323c-426f-b3cf-89b97ae24ef2-result.json new file mode 100644 index 0000000..cc76d88 --- /dev/null +++ b/allure_results/a6da569f-323c-426f-b3cf-89b97ae24ef2-result.json @@ -0,0 +1 @@ +{"name": "Восстановление пароля по email", "status": "broken", "statusDetails": {"message": "selenium.common.exceptions.ElementClickInterceptedException: Message: Element