diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..0d7af75bc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+# Python
+__pycache__/
+*.py[cod]
+*.pyo
+.pytest_cache/
+*.egg-info/
+dist/
+build/
+.eggs/
+
+# Virtual environment
+venv/
+.venv/
+env/
+
+# IDE
+.idea/
+.vscode/
+*.iml
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs and reports
+*.log
+reports/
+allure-results/
+
+# Selenium / browser
+chromedriver
+geckodriver
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 000000000..85f8d355d
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,9 @@
+import pytest
+from selenium import webdriver
+
+
+@pytest.fixture
+def driver():
+ driver = webdriver.Chrome()
+ yield driver
+ driver.quit()
\ No newline at end of file
diff --git a/helpers.py b/helpers.py
new file mode 100644
index 000000000..a9a87f0e3
--- /dev/null
+++ b/helpers.py
@@ -0,0 +1,20 @@
+import random
+import string
+
+BASE_URL = "https://stellarburgers.education-services.ru"
+LOGIN_URL = f"{BASE_URL}/login"
+REGISTER_URL = f"{BASE_URL}/register"
+FORGOT_PASSWORD_URL = f"{BASE_URL}/forgot-password"
+PROFILE_URL = f"{BASE_URL}/account/profile"
+
+
+def generate_email():
+ """Генерирует уникальный email в формате имя_фамилия_когорта_XXX@домен."""
+ digits = "".join(random.choices(string.digits, k=6))
+ return f"test_testov_999_{digits}@yandex.ru"
+
+
+def generate_password(length=8):
+ """Генерирует случайный пароль заданной длины (минимум 6 символов)."""
+ chars = string.ascii_letters + string.digits
+ return "".join(random.choices(chars, k=length))
diff --git a/locators/__init__.py b/locators/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/locators/locators.py b/locators/locators.py
new file mode 100644
index 000000000..fb1333969
--- /dev/null
+++ b/locators/locators.py
@@ -0,0 +1,81 @@
+from selenium.webdriver.common.by import By
+
+
+class MainPageLocators:
+ # Кнопка «Войти в аккаунт» в конструкторе (видна только неавторизованным)
+ LOGIN_BUTTON = (By.XPATH, "//button[text()='Войти в аккаунт']")
+
+ # Ссылка «Личный Кабинет» в шапке (тег )
+ PERSONAL_ACCOUNT_LINK = (By.XPATH, "//a[@href='/account']")
+
+ # Ссылка «Конструктор» в шапке (тег )
+ CONSTRUCTOR_LINK = (By.XPATH, "//a[@href='/'][.//p[contains(@class,'AppHeader_header__linkText')]]")
+
+ # Логотип Stellar Burgers в шапке (div-обёртка с классом header__logo)
+ LOGO = (By.XPATH, "//div[contains(@class,'AppHeader_header__logo')]")
+
+ # Вкладка «Булки» в конструкторе (кликабельный div с текстом внутри span)
+ BUNS_TAB = (By.XPATH, "//div[contains(@class,'tab_tab')]//span[text()='Булки']")
+
+ # Вкладка «Соусы» в конструкторе
+ SAUCES_TAB = (By.XPATH, "//div[contains(@class,'tab_tab')]//span[text()='Соусы']")
+
+ # Вкладка «Начинки» в конструкторе
+ FILLINGS_TAB = (By.XPATH, "//div[contains(@class,'tab_tab')]//span[text()='Начинки']")
+
+ # Заголовок раздела «Булки» в списке ингредиентов (h2)
+ BUNS_HEADING = (By.XPATH, "//h2[text()='Булки']")
+
+ # Заголовок раздела «Соусы» в списке ингредиентов (h2)
+ SAUCES_HEADING = (By.XPATH, "//h2[text()='Соусы']")
+
+ # Заголовок раздела «Начинки» в списке ингредиентов (h2)
+ FILLINGS_HEADING = (By.XPATH, "//h2[text()='Начинки']")
+
+
+class LoginPageLocators:
+ # Поле ввода email на странице входа (первый fieldset, оба поля имеют name="name")
+ EMAIL_INPUT = (By.XPATH, "//fieldset[1]//input")
+
+ # Поле ввода пароля на странице входа (второй fieldset, name="Пароль")
+ PASSWORD_INPUT = (By.XPATH, "//input[@name='Пароль']")
+
+ # Кнопка «Войти» на странице входа
+ LOGIN_BUTTON = (By.XPATH, "//button[text()='Войти']")
+
+ # Ссылка «Зарегистрироваться» на странице входа (класс Auth_link)
+ REGISTER_LINK = (By.XPATH, "//a[@href='/register']")
+
+ # Ссылка «Восстановить пароль» на странице входа
+ FORGOT_PASSWORD_LINK = (By.XPATH, "//a[@href='/forgot-password']")
+
+
+class RegisterPageLocators:
+ # Поле «Имя» на странице регистрации (первый fieldset)
+ NAME_INPUT = (By.XPATH, "//fieldset[1]//input")
+
+ # Поле «Email» на странице регистрации (второй fieldset)
+ EMAIL_INPUT = (By.XPATH, "//fieldset[2]//input")
+
+ # Поле «Пароль» на странице регистрации (третий fieldset)
+ PASSWORD_INPUT = (By.XPATH, "//fieldset[3]//input")
+
+ # Кнопка «Зарегистрироваться»
+ REGISTER_BUTTON = (By.XPATH, "//button[text()='Зарегистрироваться']")
+
+ # Ссылка «Войти» на странице регистрации (ведёт на /login)
+ LOGIN_LINK = (By.XPATH, "//a[@href='/login']")
+
+ # Сообщение об ошибке при некорректном пароле
+ PASSWORD_ERROR = (By.XPATH, "//p[text()='Некорректный пароль']")
+
+
+class ForgotPasswordPageLocators:
+ # Ссылка «Войти» на странице восстановления пароля (ведёт на /login)
+ LOGIN_LINK = (By.XPATH, "//a[@href='/login']")
+
+
+class ProfilePageLocators:
+ # Кнопка «Выйти» в боковом меню личного кабинета
+ LOGOUT_BUTTON = (By.XPATH, "//button[text()='Выход']")
+
diff --git a/praktikum/__init__.py b/praktikum/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/praktikum/bun.py b/praktikum/bun.py
new file mode 100644
index 000000000..d0a826c2d
--- /dev/null
+++ b/praktikum/bun.py
@@ -0,0 +1,10 @@
+class Bun:
+ def __init__(self, name: str, price: float):
+ self.name = name
+ self.price = price
+
+ def get_name(self) -> str:
+ return self.name
+
+ def get_price(self) -> float:
+ return self.price
diff --git a/praktikum/burger.py b/praktikum/burger.py
new file mode 100644
index 000000000..4e28963fc
--- /dev/null
+++ b/praktikum/burger.py
@@ -0,0 +1,34 @@
+from typing import List
+from praktikum.bun import Bun
+from praktikum.ingredient import Ingredient
+
+class Burger:
+ def __init__(self):
+ self.bun = None
+ self.ingredients: List[Ingredient] = []
+
+ def set_buns(self, bun: Bun):
+ self.bun = bun
+
+ def add_ingredient(self, ingredient: Ingredient):
+ self.ingredients.append(ingredient)
+
+ def remove_ingredient(self, index: int):
+ del self.ingredients[index]
+
+ def move_ingredient(self, index: int, new_index: int):
+ self.ingredients.insert(new_index, self.ingredients.pop(index))
+
+ def get_price(self) -> float:
+ price = self.bun.get_price() * 2
+ for ingredient in self.ingredients:
+ price += ingredient.get_price()
+ return price
+
+ def get_receipt(self) -> str:
+ receipt: List[str] = [f'(==== {self.bun.get_name()} ====)']
+ for ingredient in self.ingredients:
+ receipt.append(f'= {str(ingredient.get_type()).lower()} {ingredient.get_name()} =')
+ receipt.append(f'(==== {self.bun.get_name()} ====)\n')
+ receipt.append(f'Price: {self.get_price()}')
+ return '\n'.join(receipt)
diff --git a/praktikum/database.py b/praktikum/database.py
new file mode 100644
index 000000000..17a244243
--- /dev/null
+++ b/praktikum/database.py
@@ -0,0 +1,27 @@
+from typing import List
+from praktikum.bun import Bun
+from praktikum.ingredient import Ingredient
+from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE, INGREDIENT_TYPE_FILLING
+
+class Database:
+ def __init__(self):
+ self.buns: List[Bun] = []
+ self.ingredients: List[Ingredient] = []
+
+ self.buns.append(Bun("black bun", 100))
+ self.buns.append(Bun("white bun", 200))
+ self.buns.append(Bun("red bun", 300))
+
+ self.ingredients.append(Ingredient(INGREDIENT_TYPE_SAUCE, "hot sauce", 100))
+ self.ingredients.append(Ingredient(INGREDIENT_TYPE_SAUCE, "sour cream", 200))
+ self.ingredients.append(Ingredient(INGREDIENT_TYPE_SAUCE, "chili sauce", 300))
+
+ self.ingredients.append(Ingredient(INGREDIENT_TYPE_FILLING, "cutlet", 100))
+ self.ingredients.append(Ingredient(INGREDIENT_TYPE_FILLING, "dinosaur", 200))
+ self.ingredients.append(Ingredient(INGREDIENT_TYPE_FILLING, "sausage", 300))
+
+ def available_buns(self) -> List[Bun]:
+ return self.buns
+
+ def available_ingredients(self) -> List[Ingredient]:
+ return self.ingredients
diff --git a/praktikum/ingredient.py b/praktikum/ingredient.py
new file mode 100644
index 000000000..0a8350eae
--- /dev/null
+++ b/praktikum/ingredient.py
@@ -0,0 +1,14 @@
+class Ingredient:
+ def __init__(self, ingredient_type: str, name: str, price: float):
+ self.type = ingredient_type
+ self.name = name
+ self.price = price
+
+ def get_price(self) -> float:
+ return self.price
+
+ def get_name(self) -> str:
+ return self.name
+
+ def get_type(self) -> str:
+ return self.type
diff --git a/praktikum/ingredient_types.py b/praktikum/ingredient_types.py
new file mode 100644
index 000000000..84e983fff
--- /dev/null
+++ b/praktikum/ingredient_types.py
@@ -0,0 +1,2 @@
+INGREDIENT_TYPE_SAUCE = 'SAUCE'
+INGREDIENT_TYPE_FILLING = 'FILLING'
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 000000000..a635c5c03
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+pythonpath = .
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 000000000..78b9d1e15
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+pytest==8.2.0
+pytest-cov==5.0.0
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_bun.py b/tests/test_bun.py
new file mode 100644
index 000000000..db4372a31
--- /dev/null
+++ b/tests/test_bun.py
@@ -0,0 +1,23 @@
+import pytest
+from praktikum.bun import Bun
+
+
+class TestBun:
+
+ @pytest.mark.parametrize('name, price', [
+ ('black bun', 100),
+ ('white bun', 200.5),
+ ('red bun', 0),
+ ])
+ def test_get_name(self, name, price):
+ bun = Bun(name, price)
+ assert bun.get_name() == name
+
+ @pytest.mark.parametrize('name, price', [
+ ('black bun', 100),
+ ('white bun', 200.5),
+ ('red bun', 0),
+ ])
+ def test_get_price(self, name, price):
+ bun = Bun(name, price)
+ assert bun.get_price() == price
diff --git a/tests/test_burger.py b/tests/test_burger.py
new file mode 100644
index 000000000..c1bf2afd9
--- /dev/null
+++ b/tests/test_burger.py
@@ -0,0 +1,142 @@
+import pytest
+from unittest.mock import MagicMock
+from praktikum.burger import Burger
+from praktikum.bun import Bun
+from praktikum.ingredient import Ingredient
+from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE, INGREDIENT_TYPE_FILLING
+
+
+@pytest.fixture
+def mock_bun():
+ bun = MagicMock(spec=Bun)
+ bun.get_name.return_value = 'black bun'
+ bun.get_price.return_value = 100
+ return bun
+
+
+@pytest.fixture
+def mock_sauce():
+ ingredient = MagicMock(spec=Ingredient)
+ ingredient.get_name.return_value = 'hot sauce'
+ ingredient.get_price.return_value = 50
+ ingredient.get_type.return_value = INGREDIENT_TYPE_SAUCE
+ return ingredient
+
+
+@pytest.fixture
+def mock_filling():
+ ingredient = MagicMock(spec=Ingredient)
+ ingredient.get_name.return_value = 'cutlet'
+ ingredient.get_price.return_value = 100
+ ingredient.get_type.return_value = INGREDIENT_TYPE_FILLING
+ return ingredient
+
+
+@pytest.fixture
+def burger_with_bun(mock_bun):
+ burger = Burger()
+ burger.set_buns(mock_bun)
+ return burger
+
+
+class TestBurgerSetBuns:
+
+ def test_set_buns(self, mock_bun):
+ burger = Burger()
+ burger.set_buns(mock_bun)
+ assert burger.bun == mock_bun
+
+
+class TestBurgerAddIngredient:
+
+ def test_add_ingredient(self, burger_with_bun, mock_sauce):
+ burger_with_bun.add_ingredient(mock_sauce)
+ assert mock_sauce in burger_with_bun.ingredients
+
+ def test_add_multiple_ingredients(self, burger_with_bun, mock_sauce, mock_filling):
+ burger_with_bun.add_ingredient(mock_sauce)
+ burger_with_bun.add_ingredient(mock_filling)
+ assert len(burger_with_bun.ingredients) == 2
+
+
+class TestBurgerRemoveIngredient:
+
+ def test_remove_ingredient(self, burger_with_bun, mock_sauce, mock_filling):
+ burger_with_bun.add_ingredient(mock_sauce)
+ burger_with_bun.add_ingredient(mock_filling)
+ burger_with_bun.remove_ingredient(0)
+ assert mock_sauce not in burger_with_bun.ingredients
+
+ def test_remove_ingredient_reduces_count(self, burger_with_bun, mock_sauce):
+ burger_with_bun.add_ingredient(mock_sauce)
+ burger_with_bun.remove_ingredient(0)
+ assert len(burger_with_bun.ingredients) == 0
+
+
+class TestBurgerMoveIngredient:
+
+ def test_move_ingredient(self, burger_with_bun, mock_sauce, mock_filling):
+ burger_with_bun.add_ingredient(mock_sauce)
+ burger_with_bun.add_ingredient(mock_filling)
+ burger_with_bun.move_ingredient(1, 0)
+ assert burger_with_bun.ingredients[0] == mock_filling
+ assert burger_with_bun.ingredients[1] == mock_sauce
+
+
+class TestBurgerGetPrice:
+
+ def test_get_price_bun_only(self, burger_with_bun):
+ assert burger_with_bun.get_price() == 200
+
+ def test_get_price_with_ingredients(self, burger_with_bun, mock_sauce, mock_filling):
+ burger_with_bun.add_ingredient(mock_sauce)
+ burger_with_bun.add_ingredient(mock_filling)
+ assert burger_with_bun.get_price() == 350
+
+ @pytest.mark.parametrize('bun_price, ingredient_prices, expected', [
+ (100, [], 200),
+ (100, [50], 250),
+ (200, [100, 150], 650),
+ ])
+ def test_get_price_parametrized(self, bun_price, ingredient_prices, expected):
+ bun = MagicMock(spec=Bun)
+ bun.get_price.return_value = bun_price
+ bun.get_name.return_value = 'bun'
+
+ burger = Burger()
+ burger.set_buns(bun)
+
+ for price in ingredient_prices:
+ ingredient = MagicMock(spec=Ingredient)
+ ingredient.get_price.return_value = price
+ ingredient.get_name.return_value = 'ing'
+ ingredient.get_type.return_value = INGREDIENT_TYPE_SAUCE
+ burger.add_ingredient(ingredient)
+
+ assert burger.get_price() == expected
+
+
+class TestBurgerGetReceipt:
+
+ def test_get_receipt_contains_bun_name(self, burger_with_bun):
+ assert 'black bun' in burger_with_bun.get_receipt()
+
+ def test_get_receipt_contains_price(self, burger_with_bun):
+ assert 'Price: 200' in burger_with_bun.get_receipt()
+
+ def test_get_receipt_contains_ingredient_name(self, burger_with_bun, mock_sauce):
+ burger_with_bun.add_ingredient(mock_sauce)
+ assert 'hot sauce' in burger_with_bun.get_receipt()
+
+ def test_get_receipt_contains_ingredient_type(self, burger_with_bun, mock_sauce):
+ burger_with_bun.add_ingredient(mock_sauce)
+ assert 'sauce' in burger_with_bun.get_receipt()
+
+ def test_get_receipt_format(self, burger_with_bun, mock_sauce):
+ burger_with_bun.add_ingredient(mock_sauce)
+ receipt = burger_with_bun.get_receipt()
+
+ lines = receipt.split('\n')
+
+ assert lines[0] == '(==== black bun ====)'
+ assert '(==== black bun ====)' in lines
diff --git a/tests/test_constructor.py b/tests/test_constructor.py
new file mode 100644
index 000000000..6a8648735
--- /dev/null
+++ b/tests/test_constructor.py
@@ -0,0 +1,17 @@
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+from helpers import BASE_URL
+from locators.locators import MainPageLocators
+
+
+def test_navigate_to_buns(driver):
+ wait = WebDriverWait(driver, 5)
+
+ driver.get(BASE_URL)
+
+ heading = wait.until(
+ EC.visibility_of_element_located(MainPageLocators.BUNS_HEADING)
+ )
+
+ assert heading.is_displayed()
\ No newline at end of file
diff --git a/tests/test_database.py b/tests/test_database.py
new file mode 100644
index 000000000..8c52e4565
--- /dev/null
+++ b/tests/test_database.py
@@ -0,0 +1,32 @@
+from praktikum.database import Database
+from praktikum.bun import Bun
+from praktikum.ingredient import Ingredient
+
+
+class TestDatabase:
+
+ def test_available_buns_returns_list(self):
+ db = Database()
+ assert isinstance(db.available_buns(), list)
+
+ def test_available_buns_not_empty(self):
+ db = Database()
+ assert len(db.available_buns()) > 0
+
+ def test_available_buns_contains_bun_instances(self):
+ db = Database()
+ for bun in db.available_buns():
+ assert isinstance(bun, Bun)
+
+ def test_available_ingredients_returns_list(self):
+ db = Database()
+ assert isinstance(db.available_ingredients(), list)
+
+ def test_available_ingredients_not_empty(self):
+ db = Database()
+ assert len(db.available_ingredients()) > 0
+
+ def test_available_ingredients_contains_ingredient_instances(self):
+ db = Database()
+ for ingredient in db.available_ingredients():
+ assert isinstance(ingredient, Ingredient)
diff --git a/tests/test_ingredient.py b/tests/test_ingredient.py
new file mode 100644
index 000000000..cb8ebc9c5
--- /dev/null
+++ b/tests/test_ingredient.py
@@ -0,0 +1,32 @@
+import pytest
+from praktikum.ingredient import Ingredient
+from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE, INGREDIENT_TYPE_FILLING
+
+
+class TestIngredient:
+
+ @pytest.mark.parametrize('ingredient_type, name, price', [
+ (INGREDIENT_TYPE_SAUCE, 'hot sauce', 100),
+ (INGREDIENT_TYPE_FILLING, 'cutlet', 200),
+ (INGREDIENT_TYPE_SAUCE, 'sour cream', 50.5),
+ ])
+ def test_get_name(self, ingredient_type, name, price):
+ ingredient = Ingredient(ingredient_type, name, price)
+ assert ingredient.get_name() == name
+
+ @pytest.mark.parametrize('ingredient_type, name, price', [
+ (INGREDIENT_TYPE_SAUCE, 'hot sauce', 100),
+ (INGREDIENT_TYPE_FILLING, 'cutlet', 200),
+ (INGREDIENT_TYPE_SAUCE, 'sour cream', 50.5),
+ ])
+ def test_get_price(self, ingredient_type, name, price):
+ ingredient = Ingredient(ingredient_type, name, price)
+ assert ingredient.get_price() == price
+
+ @pytest.mark.parametrize('ingredient_type, name, price', [
+ (INGREDIENT_TYPE_SAUCE, 'hot sauce', 100),
+ (INGREDIENT_TYPE_FILLING, 'cutlet', 200),
+ ])
+ def test_get_type(self, ingredient_type, name, price):
+ ingredient = Ingredient(ingredient_type, name, price)
+ assert ingredient.get_type() == ingredient_type
diff --git a/tests/test_login.py b/tests/test_login.py
new file mode 100644
index 000000000..1ae2c919a
--- /dev/null
+++ b/tests/test_login.py
@@ -0,0 +1,93 @@
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+from helpers import BASE_URL, LOGIN_URL, REGISTER_URL, FORGOT_PASSWORD_URL, generate_email, generate_password
+from locators.locators import MainPageLocators, LoginPageLocators, RegisterPageLocators, ForgotPasswordPageLocators
+
+
+def register_and_get_credentials(driver):
+ wait = WebDriverWait(driver, 5)
+
+ driver.get(REGISTER_URL)
+
+ email = generate_email()
+ password = generate_password()
+
+ wait.until(EC.element_to_be_clickable(RegisterPageLocators.NAME_INPUT)).send_keys("Test User")
+ driver.find_element(*RegisterPageLocators.EMAIL_INPUT).send_keys(email)
+ driver.find_element(*RegisterPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*RegisterPageLocators.REGISTER_BUTTON).click()
+
+ wait.until(EC.url_to_be(LOGIN_URL))
+
+ return email, password
+
+
+class TestLogin:
+
+ def test_login_via_main_button(self, driver):
+ wait = WebDriverWait(driver, 5)
+
+ email, password = register_and_get_credentials(driver)
+
+ driver.get(BASE_URL)
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.LOGIN_BUTTON)).click()
+
+ wait.until(EC.visibility_of_element_located(LoginPageLocators.EMAIL_INPUT)).send_keys(email)
+ driver.find_element(*LoginPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*LoginPageLocators.LOGIN_BUTTON).click()
+
+ wait.until(EC.url_to_be(BASE_URL + "/"))
+ assert driver.current_url == BASE_URL + "/"
+
+ def test_login_via_personal_account_link(self, driver):
+ wait = WebDriverWait(driver, 5)
+
+ email, password = register_and_get_credentials(driver)
+
+ driver.get(BASE_URL)
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.PERSONAL_ACCOUNT_LINK)).click()
+ wait.until(EC.url_to_be(LOGIN_URL))
+
+ wait.until(EC.visibility_of_element_located(LoginPageLocators.EMAIL_INPUT)).send_keys(email)
+ driver.find_element(*LoginPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*LoginPageLocators.LOGIN_BUTTON).click()
+
+ wait.until(EC.url_to_be(BASE_URL + "/"))
+ assert driver.current_url == BASE_URL + "/"
+
+ def test_login_via_register_page_link(self, driver):
+ wait = WebDriverWait(driver, 5)
+
+ email, password = register_and_get_credentials(driver)
+
+ driver.get(REGISTER_URL)
+
+ wait.until(EC.element_to_be_clickable(RegisterPageLocators.LOGIN_LINK)).click()
+ wait.until(EC.url_to_be(LOGIN_URL))
+
+ wait.until(EC.visibility_of_element_located(LoginPageLocators.EMAIL_INPUT)).send_keys(email)
+ driver.find_element(*LoginPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*LoginPageLocators.LOGIN_BUTTON).click()
+
+ wait.until(EC.url_to_be(BASE_URL + "/"))
+ assert driver.current_url == BASE_URL + "/"
+
+ def test_login_via_forgot_password_link(self, driver):
+ wait = WebDriverWait(driver, 5)
+
+ email, password = register_and_get_credentials(driver)
+
+ driver.get(FORGOT_PASSWORD_URL)
+
+ wait.until(EC.element_to_be_clickable(ForgotPasswordPageLocators.LOGIN_LINK)).click()
+ wait.until(EC.url_to_be(LOGIN_URL))
+
+ wait.until(EC.visibility_of_element_located(LoginPageLocators.EMAIL_INPUT)).send_keys(email)
+ driver.find_element(*LoginPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*LoginPageLocators.LOGIN_BUTTON).click()
+
+ wait.until(EC.url_to_be(BASE_URL + "/"))
+ assert driver.current_url == BASE_URL + "/"
\ No newline at end of file
diff --git a/tests/test_personal_account.py b/tests/test_personal_account.py
new file mode 100644
index 000000000..3b2ae01a3
--- /dev/null
+++ b/tests/test_personal_account.py
@@ -0,0 +1,87 @@
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+from helpers import BASE_URL, LOGIN_URL, REGISTER_URL, generate_email, generate_password
+from locators.locators import MainPageLocators, LoginPageLocators, RegisterPageLocators, ProfilePageLocators
+
+
+def register_and_login(driver):
+ wait = WebDriverWait(driver, 7)
+
+ email = generate_email()
+ password = generate_password()
+
+ driver.get(REGISTER_URL)
+
+ wait.until(EC.visibility_of_element_located(RegisterPageLocators.NAME_INPUT)).send_keys("Test User")
+ driver.find_element(*RegisterPageLocators.EMAIL_INPUT).send_keys(email)
+ driver.find_element(*RegisterPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*RegisterPageLocators.REGISTER_BUTTON).click()
+
+ wait.until(EC.url_to_be(LOGIN_URL))
+
+ wait.until(EC.visibility_of_element_located(LoginPageLocators.EMAIL_INPUT)).send_keys(email)
+ driver.find_element(*LoginPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*LoginPageLocators.LOGIN_BUTTON).click()
+
+ wait.until(EC.visibility_of_element_located(MainPageLocators.PERSONAL_ACCOUNT_LINK))
+
+
+class TestPersonalAccount:
+
+ def test_navigate_to_profile(self, driver):
+ wait = WebDriverWait(driver, 7)
+
+ register_and_login(driver)
+
+ current_url = driver.current_url
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.PERSONAL_ACCOUNT_LINK)).click()
+ wait.until(EC.url_changes(current_url))
+
+ assert "/account" in driver.current_url
+
+ def test_navigate_to_constructor_via_link(self, driver):
+ wait = WebDriverWait(driver, 7)
+
+ register_and_login(driver)
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.PERSONAL_ACCOUNT_LINK)).click()
+ wait.until(EC.visibility_of_element_located(ProfilePageLocators.LOGOUT_BUTTON))
+
+ current_url = driver.current_url
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.CONSTRUCTOR_LINK)).click()
+ wait.until(EC.url_changes(current_url))
+
+ assert driver.current_url == BASE_URL + "/"
+
+ def test_navigate_to_constructor_via_logo(self, driver):
+ wait = WebDriverWait(driver, 7)
+
+ register_and_login(driver)
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.PERSONAL_ACCOUNT_LINK)).click()
+ wait.until(EC.visibility_of_element_located(ProfilePageLocators.LOGOUT_BUTTON))
+
+ current_url = driver.current_url
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.LOGO)).click()
+ wait.until(EC.url_changes(current_url))
+
+ assert driver.current_url == BASE_URL + "/"
+
+ def test_logout(self, driver):
+ wait = WebDriverWait(driver, 7)
+
+ register_and_login(driver)
+
+ wait.until(EC.element_to_be_clickable(MainPageLocators.PERSONAL_ACCOUNT_LINK)).click()
+ wait.until(EC.visibility_of_element_located(ProfilePageLocators.LOGOUT_BUTTON))
+
+ current_url = driver.current_url
+
+ wait.until(EC.element_to_be_clickable(ProfilePageLocators.LOGOUT_BUTTON)).click()
+ wait.until(EC.url_changes(current_url))
+
+ assert "/login" in driver.current_url
\ No newline at end of file
diff --git a/tests/test_registration.py b/tests/test_registration.py
new file mode 100644
index 000000000..47dcbd89d
--- /dev/null
+++ b/tests/test_registration.py
@@ -0,0 +1,40 @@
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+from helpers import REGISTER_URL, LOGIN_URL, generate_email, generate_password
+from locators.locators import RegisterPageLocators
+
+
+class TestRegistration:
+
+ def test_successful_registration(self, driver):
+ wait = WebDriverWait(driver, 5)
+
+ driver.get(REGISTER_URL)
+
+ email = generate_email()
+ password = generate_password()
+
+ wait.until(EC.element_to_be_clickable(RegisterPageLocators.NAME_INPUT)).send_keys("Test User")
+ driver.find_element(*RegisterPageLocators.EMAIL_INPUT).send_keys(email)
+ driver.find_element(*RegisterPageLocators.PASSWORD_INPUT).send_keys(password)
+ driver.find_element(*RegisterPageLocators.REGISTER_BUTTON).click()
+
+ wait.until(EC.url_to_be(LOGIN_URL))
+ assert driver.current_url == LOGIN_URL
+
+ def test_registration_with_invalid_password(self, driver):
+ wait = WebDriverWait(driver, 5)
+
+ driver.get(REGISTER_URL)
+
+ wait.until(EC.element_to_be_clickable(RegisterPageLocators.NAME_INPUT)).send_keys("Test User")
+ driver.find_element(*RegisterPageLocators.EMAIL_INPUT).send_keys(generate_email())
+ driver.find_element(*RegisterPageLocators.PASSWORD_INPUT).send_keys("123")
+ driver.find_element(*RegisterPageLocators.REGISTER_BUTTON).click()
+
+ error = wait.until(
+ EC.visibility_of_element_located(RegisterPageLocators.PASSWORD_ERROR)
+ )
+
+ assert error.is_displayed()
\ No newline at end of file