Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ env/
.mypy_cache/
.pytest_cache/
__pycache__/
.env
allure/
13 changes: 13 additions & 0 deletions _secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import os

from dotenv import load_dotenv

load_dotenv()

try:

LOGIN = os.environ["LOGIN"]
PASSWORD = os.environ["PASSWORD"]
USER_PASSWORD = os.environ["DEFAULT_USER_PASSWORD"]
except KeyError as e:
raise ValueError(f"Отсутствует необходимая переменная окружения: {e}") from None
Empty file added configs/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions configs/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from dataclasses import dataclass

from _secrets import LOGIN, PASSWORD, USER_PASSWORD

base_url = "https://xieffect.ru/"

app_base_url = "https://app.xieffect.ru"
signin_page = f"{app_base_url}/signin"
signup_page = f"{app_base_url}/signup"
recovery_page = f"{app_base_url}/reset-password"


@dataclass
class Credentials:
test_login: str = LOGIN
test_password: str = PASSWORD
user_password: str = USER_PASSWORD
35 changes: 35 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging

import pytest
from _pytest.fixtures import SubRequest
from selenium import webdriver


def pytest_addoption(parser):
parser.addoption("--browser", action="store", default="chrome")
parser.addoption("--headed", action="store_true")


@pytest.fixture(scope="function", autouse=True)
def driver(request: SubRequest):
browser_name = request.config.getoption("--browser").lower()
headed_option = request.config.getoption("--headed")
logging.info(f"Options {request.config}")

match browser_name:
case "chrome":
chrome_options = webdriver.ChromeOptions()
if not headed_option:
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(options=chrome_options)
case "firefox":
firefox_options = webdriver.FirefoxOptions()
if not headed_option:
firefox_options.headless = True
driver = webdriver.Firefox()
case _:
raise ValueError(f"Unknown browser {browser_name}")
driver.maximize_window()
request.cls.driver = driver
yield driver
driver.quit()
Empty file added data/__init__.py
Empty file.
42 changes: 42 additions & 0 deletions data/person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from dataclasses import dataclass, field

from faker import Faker

from configs.config import Credentials

faker_en = Faker()


@dataclass
class Person:
"""
Represents a person with their basic information.

Attributes:
display_name (str): Full name of the person.
username (str): Automatically generated username based on the display name.
community_name (str): Name of the person's community.
email (str): Email address of the person.
password (str): Password associated with the account.
"""

display_name: str
username: str = field(init=False)
community_name: str = field(init=False)
email: str
password: str

def __post_init__(self):
self.username = self.display_name.replace(" ", "_").lower()
self.community_name = self.display_name + " community"


class PersonCreateInfo:

@staticmethod
def user_create_info() -> Person:
return Person(
display_name=faker_en.name(),
email=faker_en.email(),
password=Credentials().user_password,
)
Empty file added pages/__init__.py
Empty file.
108 changes: 108 additions & 0 deletions pages/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from typing import Any

from allure_commons.types import AttachmentType
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

import allure


class BasePage:

def __init__(self, driver):
self.driver: WebDriver = driver
self.wait = WebDriverWait(driver, 20, poll_frequency=0.1)

def go_to_page(self, url: str | None = None) -> None:
if url is None:
url = self.page_url
with allure.step(f"Go to {url=!s}"):
self.driver.get(url)

def is_page_opened(self):
with allure.step(f"Page {self.page_url=!s} is opened"):
self.wait.until(EC.url_to_be(self.page_url))

def make_screenschot(self, screenshot_name):
allure.attach(
body=self.driver.get_screenshot_as_png(),
name=screenshot_name,
attachment_type=AttachmentType.PNG,
)

@allure.step("Get text from element")
def get_element_text(self, locator: str) -> str:
element = self.get_element_by(locator)
return element.text

@staticmethod
def generate_locator(locator_value: str, find_by: str = "xpath") -> tuple[Any, str]:
"""
Generates a locator for finding an element on the web page.

:param locator_value: The value of the locator (e.g., XPath expression).
:param find_by: The type of the locator. Defaults to 'xpath'.
:return: A tuple containing the By object and the locator value.
:raises ValueError: If the provided locator type is unknown.
"""
find_by = find_by.lower()
locating = {
"css": By.CSS_SELECTOR,
"xpath": By.XPATH,
"class_name": By.CLASS_NAME,
"id": By.ID,
"link_text": By.LINK_TEXT,
"name": By.NAME,
"partial_link_text": By.PARTIAL_LINK_TEXT,
"tag_name": By.TAG_NAME,
}
try:
by_type = locating[find_by]

except KeyError:
raise ValueError(f"Unknown locator type: {find_by}")
result = by_type, locator_value
return result

def get_element_by(self, locator: str, find_by: str = "xpath") -> WebElement:
"""
Finds an element on the web page using the specified locator
and returns it once it's visible.

:param locator: The value of the locator (e.g., CSS selector
or XPath expression).
:param find_by: The type of the locator. Defaults to 'xpath'.
:return: The located WebElement.
:raises ValueError: If the provided locator type is unknown.
"""
find_by = find_by.lower()
locating = {
"css": By.CSS_SELECTOR,
"xpath": By.XPATH,
"class_name": By.CLASS_NAME,
"id": By.ID,
"link_text": By.LINK_TEXT,
"name": By.NAME,
"partial_link_text": By.PARTIAL_LINK_TEXT,
"tag_name": By.TAG_NAME,
}

try:
by_type = locating[find_by]
except KeyError:
raise ValueError(f"Unknown locator type: {find_by}")
element = self.wait.until(
EC.visibility_of_element_located((by_type, locator))
) # to be visible
return element

def send_keys(self, locator: str, value: str) -> None:
element = self.get_element_by(locator)
self.wait.until(EC.element_to_be_clickable(element)).send_keys(value)

def click(self, locator: str) -> None:
element = self.get_element_by(locator)
self.wait.until(EC.element_to_be_clickable(element)).click()
65 changes: 65 additions & 0 deletions pages/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from dataclasses import dataclass, field

from selenium.webdriver.support import expected_conditions as EC

import allure
from pages.base import BasePage


@dataclass
class UserProfileMenu:
to_profile_btn: str = '//div[@id="user-profile-menu"]' # root
display_name: str = f"{to_profile_btn}//child::node()[2]/child::node()[1]"
user_name: str = f"{to_profile_btn}//child::node()[2]/child::node()[2]"


@dataclass
class CommunityProfileMenu:
root: str = '//div[@id="community-profile"]'
community_profile_btn: str = f'{root}/parent::div[@type="button"]'
community_profile_title: str = f"{root}/div[1]"


@dataclass
class Locators:
title: str = "//h2"
community_name: str = "//h2//following-sibling::node()/p"
user_role: str = "//header/p"
community_profile_menu: CommunityProfileMenu = field(
default_factory=CommunityProfileMenu
)
user_profile_menu: UserProfileMenu = field(default_factory=UserProfileMenu)
to_onboarding_btn: str = (
'//span[text()="Пройти обучение"]//parent::node()[@type="button"]'
)


class DashboardPage(BasePage):
"""Class for dashboard page"""

locators: Locators = Locators()

@allure.step("Check we at dashboard page")
def is_dashboard_page(self) -> bool:
title = self.generate_locator(self.locators.title)
return self.wait.until(
EC.text_to_be_present_in_element(title, "Добро пожаловать в сообщество")
)

def get_community_name(self) -> str:
return self.get_element_text(self.locators.community_name)

def get_user_name(self) -> str:
return self.get_element_text(self.locators.user_profile_menu.user_name)

def get_display_name(self) -> str:
return self.get_element_text(self.locators.user_profile_menu.display_name)

def get_community_profile_title(self) -> str:
return self.get_element_text(
self.locators.community_profile_menu.community_profile_title
)

@allure.step('Verify that the user role displayed on the UI is "Administrator"')
def is_admin_user_role(self) -> bool:
return "администратор" in self.get_element_text(self.locators.user_role)
15 changes: 15 additions & 0 deletions pages/recovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass

from pages.base import BasePage


@dataclass
class Locators:
recovery_form_title: str = '//h1[text()="Восстановление"]'
input_email: str = '//input[@name="email"]'
submit_btn: str = '//button[@type="submit"]'
signin_link: str = '//a[text()="Войти"]'


class Recovery(BasePage):
locators: Locators = Locators()
36 changes: 36 additions & 0 deletions pages/signin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from dataclasses import dataclass

from selenium.webdriver.support import expected_conditions as EC

import allure
import configs.config
from pages.base import BasePage


@dataclass
class Locators:
signin_form_title: str = '//h1[text()="Вход в аккаунт"]'
input_email: str = '//input[@name="email"]'
input_password: str = '//input[@name="password"]'
reset_password: str = '//a[@href="/reset-password"]'
signup_link: str = '//a[@id="to-signup-link"]'
submit_btn: str = '//button[@type="submit"]'


class SigninPage(BasePage):
"""Class for signin page"""

page_url = configs.config.signin_page
locators = Locators()

@allure.step("Enter login")
def enter_login(self, login: str) -> None:
self.send_keys(self.locators.input_email, login)

@allure.step("Enter password")
def enter_password(self, password: str) -> None:
self.send_keys(self.locators.input_password, password)

@allure.step("Click submit button")
def click_submit_button(self) -> None:
self.click(self.locators.submit_btn)
49 changes: 49 additions & 0 deletions pages/signup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from dataclasses import dataclass

from selenium.webdriver.support import expected_conditions as EC

import allure
import configs.config
import data
from configs.config import signin_page
from data.person import Person, PersonCreateInfo
from pages.signin import SigninPage


@dataclass
class Locators:
signup_form_title = "//h1"
input_username: str = '//input[@name="username"]'
input_email: str = '//input[@name="email"]'
input_password: str = '//input[@name="password"]'
signin_link: str = '//a[@href="/signin"]'
submit_btn: str = '//button[@type="submit"]'


class SignupPage(SigninPage):
"""Class for signup page"""

page_url = configs.config.signup_page
locators = Locators()

@allure.step("Enter username")
def enter_username(self, username: str) -> None:
self.send_keys(self.locators.input_username, username)

@allure.step("Enter login")
def enter_email(self, email: str) -> None:
self.send_keys(self.locators.input_email, email)

@allure.step("Enter password")
def enter_password(self, password: str) -> None:
self.send_keys(self.locators.input_password, password)

@allure.step("Click submit button")
def click_submit_button(self) -> None:
self.click(self.locators.submit_btn)

def go_to_signin_page(self) -> None: ...

def is_signup_page(self) -> bool:
title = ("xpath", self.locators.signup_form_title)
return self.wait.until(EC.text_to_be_present_in_element(title, "Регистрация"))
Loading