Initial commit: добавлены файлы проекта

pull/1/head
Radislav 2025-07-21 07:44:09 +03:00
commit ee04c0c808
201 changed files with 109718 additions and 0 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
ENV=test
AUTH_LOGIN = admin
AUTH_PASSWORD = admin

3
.env.69 Normal file
View File

@ -0,0 +1,3 @@
ENV=develop
AUTH_LOGIN = admin
AUTH_PASSWORD = admin123

10
changelog.txt Normal file
View File

@ -0,0 +1,10 @@
====== V3 =========
- pages\service_status_tab.py: Добавлено получение количества строк в таблице - get_rows_count(self)
- tests\e2e\test_service_status_tab.py: Добавлен тест проверки подсветки строк в таблице при наведении на них курсора - test_service_status_table_row_highlighting(self, browser)
- data\roles_dict.py: Добавлена роль "user"
- elements\toolbar_button_element.py переименован в tooltip_button_elememt.py, класс ToolbarButton стал TooltipButton, в сигнатуру функции check_tooltip_with_text добавился аргумент
tooltiplocator
- components\toolbar_component.py - добавлен tooltiplocator в сигнатуру функции check_button_tooltip, изменены функции add_button и get_button_by_name
- pages\users_tab.py - переписана функция should_be_toolbar_buttons
- pages\session_tab.py - вкладка "Сессии"
- tests\e2e\test_sessions_tab.py - тест вкладки "Сессии"

2
components/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Auto-generated by fix_python_project.py
"""Package initialization."""

View File

@ -0,0 +1,89 @@
from playwright.sync_api import Page, expect
from components.base_component import BaseComponent
from elements.text_element import Text
from tools.logger import get_logger
logger = get_logger("ALERT")
class AlertComponent(BaseComponent):
"""Компонент для работы с alert-окнами.
Поддерживает различные типы alert-окон: error, success, info, warning.
Атрибуты:
page: экземпляр страницы Playwright
alert_type: тип alert-окна (error/success/info/warning)
text: текстовый элемент сообщения alert-окна
"""
def __init__(self, page: Page, alert_type: str):
"""Инициализация компонента alert-окна.
Args:
page: экземпляр страницы Playwright
alert_type: тип alert-окна (error/success/info/warning)
Raises:
ValueError: если передан неподдерживаемый тип alert-окна
"""
super().__init__(page)
alert_types = ["error", "success", "info", "warning"]
if alert_type not in alert_types:
raise ValueError("Unsupported type of alert window")
self.alert_type = alert_type
self.text = Text(page, f"//div[@class='v-alert {self.alert_type}']/div", "Alert message")
# Действия:
def get_text(self):
"""Получение текста сообщения из alert-окна.
Returns:
str: текст сообщения alert-окна
"""
return self.text.get_text(0)
# Проверки:
def check_presence(self, text):
"""Проверка наличия alert-окна с заданным текстом.
Args:
text: текст для проверки (если пустая строка - проверяется только наличие окна)
Raises:
AssertionError: если alert-окно не найдено
"""
msg = f"No {self.alert_type} alert window on page"
if text == "":
expect(self.page.get_by_role("alert")).to_be_visible(), msg
else:
expect(self.page.get_by_role("alert").filter(has_text=text)).to_be_visible(), msg
def check_absence(self, text, timeout=30000):
"""Проверка отсутствия alert-окна с заданным текстом.
Args:
text: текст для проверки
timeout: время ожидания исчезновения (в миллисекундах)
Raises:
AssertionError: если alert-окно не исчезает в течение заданного времени
"""
seconds = int(timeout/1000)
msg = f"Alert {self.alert_type} window should disappear after {seconds} seconds"
expect(self.page.get_by_role("alert").filter(has_text=text)).to_be_hidden(timeout=timeout), msg
def check_text(self, alert_text):
"""Проверка точного соответствия текста в alert-окне.
Args:
alert_text: ожидаемый текст сообщения
Raises:
AssertionError: если текст не соответствует ожидаемому
"""
self.text.check_have_text(alert_text, f"Unexpected message in alert {self.alert_type} window")

View File

@ -0,0 +1,172 @@
from playwright.sync_api import Page, Locator, expect
from tools.logger import get_logger
logger = get_logger("BASE_COMPONENT")
class BaseComponent:
"""Базовый компонент для работы с элементами страницы.
Предоставляет общие методы для взаимодействия с элементами:
- получение локаторов
- проверка видимости элементов
- работа с прокруткой
Атрибуты:
page: экземпляр страницы Playwright
"""
def __init__(self, page: Page):
"""Инициализация базового компонента.
Args:
page: экземпляр страницы Playwright
"""
self.page = page
# Действия:
def get_locator(self, locator: str | Locator) -> Locator:
"""Получение объекта Locator из строки или существующего Locator.
Args:
locator: строка с CSS/XPath селектором или объект Locator
Returns:
Locator: объект для работы с элементом
Raises:
TypeError: если передан некорректный тип локатора
"""
if isinstance(locator, Locator):
return locator
elif isinstance(locator, str):
return self.page.locator(locator)
else:
raise TypeError("locator value should be string type or Locator type")
# Закомментированный код сохранен без изменений
# def wait_for_all_elements(self, locator: Locator, timeout=5000):
# loc = self.get_locator(locator)
# elements = self.page.locator(loc).all()
#
# for element in elements:
# self.page.locator(loc).wait_for(timeout=timeout)
#
# return elements
# Проверки:
def check_presence(self, locator, msg):
"""Проверка видимости элемента на странице.
Args:
locator: локатор элемента (строка или объект Locator)
msg: сообщение об ошибке при неудачной проверке
Raises:
AssertionError: если элемент не виден на странице
"""
loc = self.get_locator(locator)
expect(loc).to_be_visible(visible=True, timeout=12000), msg
def is_scrollable_vertically(self, locator) -> bool:
"""Проверка возможности вертикальной прокрутки элемента.
Args:
locator: локатор элемента
Returns:
bool: True если элемент можно прокрутить вертикально
"""
loc = self.get_locator(locator)
return loc.evaluate("el => el.scrollHeight > el.clientHeight")
def is_scrollable_horizontally(self, locator) -> bool:
"""Проверка возможности горизонтальной прокрутки элемента.
Args:
locator: локатор элемента
Returns:
bool: True если элемент можно прокрутить горизонтально
"""
loc = self.get_locator(locator)
return loc.evaluate("el => el.scrollWidth > el.clientWidth")
# Методы прокрутки:
def scroll_up(self, locator):
"""Прокрутка элемента вверх.
Args:
locator: локатор элемента
Raises:
AssertionError: если прокрутка не выполнена до конца
"""
loc = self.get_locator(locator)
loc.evaluate("el => el.scrollTo(0, 0)")
loc.wait_for(timeout=2000)
# Проверка позиции прокрутки
scroll_position = loc.evaluate("el => el.scrollTop")
assert scroll_position == 0, "Invalid postion after scroll up"
def scroll_down(self, locator):
"""Прокрутка элемента вниз.
Args:
locator: локатор элемента
Raises:
AssertionError: если прокрутка не выполнена до конца
"""
loc = self.get_locator(locator)
loc.evaluate("el => el.scrollTo(0, el.scrollHeight)")
loc.wait_for(timeout=2000)
# Проверка позиции прокрутки
scroll_position = loc.evaluate("el => el.scrollTop")
assert scroll_position > 0, "Invalid postion after scroll down"
def scroll_left(self, locator):
"""Прокрутка элемента влево.
Args:
locator: локатор элемента
Raises:
AssertionError: если прокрутка не выполнена до конца
"""
loc = self.get_locator(locator)
width = loc.evaluate("el => el.scrollWidth")
loc.scroll_into_view_if_needed()
self.page.mouse.wheel(-width, 0)
loc.wait_for(timeout=2000)
# Проверка позиции прокрутки
scroll_position = loc.evaluate("el => el.scrollLeft")
assert scroll_position == 0, "Invalid postion after scroll left"
def scroll_right(self, locator):
"""Прокрутка элемента вправо.
Args:
locator: локатор элемента
Raises:
AssertionError: если прокрутка не выполнена до конца
"""
loc = self.get_locator(locator)
width = loc.evaluate("el => el.scrollWidth")
loc.scroll_into_view_if_needed()
self.page.mouse.wheel(width, 0)
loc.wait_for(timeout=2000)
# Проверка позиции прокрутки
scroll_position = loc.evaluate("el => el.scrollLeft")
max_scroll_x = loc.evaluate("el => el.scrollWidth - el.clientWidth")
assert scroll_position >= max_scroll_x, "Invalid postion after scroll right"

View File

@ -0,0 +1,44 @@
from playwright.sync_api import Page
from components.base_component import BaseComponent
from elements.button_element import Button
from tools.logger import get_logger
logger = get_logger("USER_CARD")
class CardComponent(BaseComponent):
"""Компонент карточки пользователя.
Предоставляет методы для взаимодействия с элементами карточки пользователя.
Атрибуты:
page: экземпляр страницы Playwright
logout_button: кнопка выхода из системы
"""
def __init__(self, page: Page):
"""Инициализация компонента карточки пользователя.
Args:
page: экземпляр страницы Playwright
"""
super().__init__(page)
self.logout_button = Button(
page,
page.get_by_role("button", name="Выйти"),
"logout button"
)
# Действия:
def click_logout_button(self):
"""Нажатие кнопки выхода из системы.
Выполняет клик по кнопке 'Выйти' в карточке пользователя.
"""
self.logout_button.click()
# Проверки:
# (Методы проверок могут быть добавлены здесь в будущем)

View File

@ -0,0 +1,95 @@
from playwright.sync_api import Page
from components.base_component import BaseComponent
from elements.button_element import Button
from elements.text_element import Text
from locators.confirm_locators import ConfirmLocators
from tools.logger import get_logger
logger = get_logger("CONFIRM_WINDOW")
class ConfirmComponent(BaseComponent):
"""Компонент окна подтверждения действий.
Предоставляет методы для взаимодействия с диалоговыми окнами подтверждения,
содержащими кнопки отмены и подтверждения действия.
Атрибуты:
page: экземпляр страницы Playwright
title: текстовый элемент заголовка окна
text: текстовый элемент основного сообщения
close_button: кнопка закрытия окна
cancel_button: кнопка отмены действия
allow_button: кнопка подтверждения действия
"""
def __init__(self, page: Page, cancel_button_text: str, allow_button_text: str):
"""Инициализация компонента окна подтверждения.
Args:
page: экземпляр страницы Playwright
cancel_button_text: текст на кнопке отмены
allow_button_text: текст на кнопке подтверждения
"""
super().__init__(page)
self.title = Text(page, ConfirmLocators.TITLE, "confirm title")
self.text = Text(page, ConfirmLocators.TEXT, "confirm text")
self.close_button = Button(page, ConfirmLocators.BUTTON_CLOSE, "confirm close button")
self.cancel_button = Button(
page,
page.get_by_role("button", name=cancel_button_text).first,
"confirm cancel button"
)
self.allow_button = Button(
page,
page.get_by_role("button", name=allow_button_text).first,
"confirm allow button"
)
# Действия:
def click_allow_button(self):
"""Нажатие кнопки подтверждения действия.
Выполняет клик по кнопке с текстом, переданным в allow_button_text.
"""
self.allow_button.click()
def click_cancel_button(self):
"""Нажатие кнопки отмены действия.
Выполняет клик по кнопке с текстом, переданным в cancel_button_text.
"""
self.cancel_button.click()
def click_close_button(self):
"""Нажатие кнопки закрытия окна подтверждения."""
self.close_button.click()
# Проверки:
def check_title(self, title, msg):
"""Проверка текста заголовка окна подтверждения.
Args:
title: ожидаемый текст заголовка
msg: сообщение об ошибке при несоответствии
Raises:
AssertionError: если текст заголовка не соответствует ожидаемому
"""
self.title.check_have_text(title, msg)
def check_text(self, text, msg):
"""Проверка текста сообщения в окне подтверждения.
Args:
text: ожидаемый текст сообщения
msg: сообщение об ошибке при несоответствии
Raises:
AssertionError: если текст сообщения не соответствует ожидаемому
"""
self.text.check_have_text(text, msg)

View File

@ -0,0 +1,100 @@
from playwright.sync_api import Page
import json
import jsondiff
from components.base_component import BaseComponent
from tools.logger import get_logger
logger = get_logger("JSON_CONTAINER")
class JsonContainerComponent(BaseComponent):
"""Компонент для работы с JSON-данными на странице.
Предоставляет методы для чтения и проверки JSON-данных,
отображаемых в специальных контейнерах на странице.
Атрибуты:
page: экземпляр страницы Playwright
"""
def __init__(self, page: Page):
"""Инициализация JSON-контейнера.
Args:
page: экземпляр страницы Playwright
"""
super().__init__(page)
# Действия:
def read_data(self, locator):
"""Чтение и форматирование JSON-данных из указанного локатора.
Args:
locator: локатор элемента, содержащего JSON-данные
Returns:
dict: распарсенный JSON-объект
Raises:
json.JSONDecodeError: если данные не могут быть преобразованы в JSON
"""
def format_json_string(json_string):
"""Вспомогательная функция для форматирования строки JSON.
Args:
json_string: сырая строка с JSON-данными
Returns:
str: отформатированная строка JSON
"""
substrings = json_string.splitlines()
formatted_string_list = []
last_substring = substrings.pop()
for substring in substrings:
if substring.find(':') == -1:
if substring == '{':
formatted_string_list.append(substring)
elif substring == '}':
s1 = formatted_string_list.pop()
formatted_string_list.append(s1.rstrip(','))
formatted_string_list.append(substring + ',')
else:
formatted_string_list.append(substring + ',')
continue
key, value = substring.split(':')
s = ':'.join(['"' + key + '" ', " " + value])
if value == '{':
formatted_string_list.append(s)
else:
formatted_string_list.append(s + ',')
s2 = formatted_string_list.pop()
formatted_string_list.append(s2.rstrip(','))
formatted_string_list.append(last_substring)
return " ".join(formatted_string_list)
# Чтение JSON-содержимого из рабочей области
loc = self.get_locator(locator)
json_string = loc.inner_text()
formatted_json_string = format_json_string(json_string)
return json.loads(formatted_json_string)
# Проверки:
def check_json_equals(self, actual, expected, msg):
"""Сравнение JSON-объектов на идентичность.
Args:
actual: фактический JSON-объект
expected: ожидаемый JSON-объект
msg: сообщение об ошибке
Raises:
AssertionError: если объекты не идентичны
"""
diff = jsondiff.diff(expected, actual, syntax='symmetric')
assert len(diff) == 0, f"{msg}. DIFF is {diff}"

View File

@ -0,0 +1,188 @@
from playwright.sync_api import Page
from components.base_component import BaseComponent
from components.toolbar_component import ToolbarComponent
from elements.button_element import Button
from locators.modal_window_locators import ModalWindowLocators
from tools.logger import get_logger
logger = get_logger("MODAL_WINDOW")
class ModalWindowComponent(BaseComponent):
"""Компонент модального окна.
Предоставляет методы для работы с модальными окнами:
- управление содержимым и кнопками
- прокрутка содержимого
- проверка элементов интерфейса
Атрибуты:
page: экземпляр страницы Playwright
toolbar: компонент панели инструментов окна
content_items: словарь элементов содержимого
buttons: список кнопок окна
"""
def __init__(self, page: Page):
"""Инициализация компонента модального окна.
Args:
page: экземпляр страницы Playwright
"""
super().__init__(page)
self.toolbar = ToolbarComponent(page, "")
self.content_items = {}
self.buttons = []
# Действия:
def add_content_item(self, name, item):
"""Добавление элемента содержимого в окно.
Args:
name: имя элемента
item: объект элемента
"""
self.content_items[name] = item
def get_content_item(self, name):
"""Получение элемента содержимого по имени.
Args:
name: имя элемента
Returns:
Элемент содержимого или None, если не найден
"""
return self.content_items.get(name)
def add_toolbar_title(self, title: str):
"""Добавление заголовка в панель инструментов.
Args:
title: текст заголовка
"""
self.toolbar.add_title(title)
def add_toolbar_button(self, locator, name):
"""Добавление кнопки в панель инструментов.
Args:
locator: локатор кнопки
name: имя кнопки
"""
self.toolbar.add_button(locator, name)
def add_button(self, locator, name):
"""Добавление кнопки в окно.
Args:
locator: локатор кнопки
name: имя кнопки
"""
self.buttons.append(Button(self.page, locator, name))
def get_button_by_name(self, name) -> Button | None:
"""Поиск кнопки по имени.
Args:
name: имя кнопки
Returns:
Button: найденная кнопка или None
"""
for button in self.buttons:
if button.name == name:
return button
return None
def click_button(self, name):
"""Нажатие кнопки по имени.
Args:
name: имя кнопки
Raises:
AssertionError: если кнопка не найдена
"""
button = self.get_button_by_name(name)
if button is None:
assert False, f"Button with name '{name}' not found"
button.click()
def click_toolbar_close_button(self):
"""Нажатие кнопки закрытия в панели инструментов."""
self.toolbar.click_button("close")
def scroll_window_down(self):
"""Прокрутка содержимого окна вниз."""
self.scroll_down(ModalWindowLocators.MODAL_WINDOW)
def scroll_window_up(self):
"""Прокрутка содержимого окна вверх."""
self.scroll_up(ModalWindowLocators.MODAL_WINDOW)
def scroll_window_left(self):
"""Прокрутка содержимого окна влево."""
self.scroll_left(ModalWindowLocators.MODAL_WINDOW)
def scroll_window_right(self):
"""Прокрутка содержимого окна вправо."""
self.scroll_right(ModalWindowLocators.MODAL_WINDOW)
# Проверки:
def check_window_vertical_scrolling(self):
"""Проверка возможности вертикальной прокрутки.
Returns:
bool: True если прокрутка возможна
"""
return self.is_scrollable_vertically(ModalWindowLocators.MODAL_WINDOW)
def check_window_horizontal_scrolling(self):
"""Проверка возможности горизонтальной прокрутки.
Returns:
bool: True если прокрутка возможна
"""
return self.is_scrollable_horizontally(ModalWindowLocators.MODAL_WINDOW)
def check_by_window_title(self):
"""Проверка наличия окна по заголовку.
Raises:
AssertionError: если окно не найдено
"""
self.toolbar.check_presence(f"Modal window with '{self.toolbar.title}' is missing")
def check_button_presence(self, name):
"""Проверка наличия кнопки по имени.
Args:
name: имя кнопки
Raises:
AssertionError: если кнопка не найдена
"""
button = self.get_button_by_name(name)
if button is None:
assert False, f"Button with name '{name}' not found"
button.check_presence(f"Button with name '{name}' is missing")
def check_toolbar_button_presence(self, name):
"""Проверка наличия кнопки в панели инструментов.
Args:
name: имя кнопки
"""
self.toolbar.check_button_presence(name)
def check_toolbar_button_tooltip(self, name, tooltip):
"""Проверка подсказки кнопки в панели инструментов.
Args:
name: имя кнопки
tooltip: ожидаемый текст подсказки
"""
self.toolbar.check_button_tooltip(name, tooltip)

View File

@ -0,0 +1,85 @@
from playwright.sync_api import Page
from components.base_component import BaseComponent
from locators.navigation_panel_locators import NavigationPanelLocators
from tools.logger import get_logger
logger = get_logger("NAVIGATION_PANEL")
class NavigationPanelComponent(BaseComponent):
"""Компонент панели навигации.
Предоставляет методы для взаимодействия с элементами навигационной панели.
Наследуется от BaseComponent.
Атрибуты:
page: Page - экземпляр страницы Playwright
"""
def __init__(self, page: Page):
"""Инициализация компонента панели навигации.
Args:
page: Page - экземпляр страницы Playwright
"""
super().__init__(page)
# Действия:
def get_item_names(self, locator):
"""Получает тексты всех элементов по указанному локатору.
Args:
locator: Локатор для поиска элементов
Returns:
list: Список текстов элементов
"""
loc = self.get_locator(locator)
return loc.all_inner_texts()
def click_item(self, locator, item_name):
"""Кликает по элементу с указанным текстом.
Args:
locator: Локатор для поиска элемента
item_name: Текст элемента для клика
"""
loc = self.get_locator(locator)
loc.get_by_text(item_name).click()
def click_sub_item(self, locator, sublevel_number, item_name):
"""Кликает по вложенному элементу с указанным текстом.
Args:
locator: Локатор для поиска элемента
sublevel_number: Уровень вложенности (1 или 2)
item_name: Текст элемента для клика
Raises:
ValueError: Если указан недопустимый уровень вложенности
"""
root_locator = self.get_locator(NavigationPanelLocators.NODE_ROOT)
children_locator = self.get_locator(NavigationPanelLocators.NODE_CHILDREN)
loc = self.get_locator(locator)
if sublevel_number == 1:
loc.locator(root_locator).get_by_text(item_name).click()
elif sublevel_number == 2:
loc.locator(children_locator).locator(root_locator).get_by_text(item_name).click()
else:
raise ValueError("the navigation panel has two levels of nesting only")
# Проверки:
def check_item_visibility(self, locator, item_name):
"""Проверяет видимость элемента с указанным текстом.
Args:
locator: Локатор для поиска элемента
item_name: Текст элемента для проверки
"""
loc = self.get_locator(locator).get_by_text(item_name)
msg = f"Navigation panel item '{item_name}' is not visible"
self.check_presence(loc, msg)

View File

@ -0,0 +1,114 @@
from playwright.sync_api import Page, expect
from components.base_component import BaseComponent
from tools.logger import get_logger
logger = get_logger("TABLE")
class TableComponent(BaseComponent):
"""Компонент таблицы.
Предоставляет методы для взаимодействия с таблицами и проверки их состояния.
Наследуется от BaseComponent.
Атрибуты:
page: Page - экземпляр страницы Playwright
"""
def __init__(self, page: Page):
"""Инициализация компонента таблицы.
Args:
page: Page - экземпляр страницы Playwright
"""
super().__init__(page)
# Действия:
def read(self, locator) -> []:
"""Читает данные из таблицы, включая заголовки и содержимое ячеек.
Args:
locator: Локатор таблицы
Returns:
list: Двумерный список с данными таблицы (первый элемент - заголовки)
"""
table_data = []
table = self.get_locator(locator)
# Чтение заголовка таблицы
header_cells = table.locator("//thead/tr")
header_cell_text = header_cells.nth(0).inner_text()
header_data = header_cell_text.split('\n')
table_data.append(header_data)
# Чтение ячеек таблицы по строкам
rows = table.locator("//tbody/tr")
for i in range(rows.count()):
row = rows.nth(i)
cells = row.locator("td")
row_data = []
for j in range(cells.count()):
cell_text = cells.nth(j).inner_text()
row_data.append(cell_text)
table_data.append(row_data)
return table_data
# Проверки:
def check_first_row_visibility(self, locator):
"""Проверяет видимость первой строки таблицы.
Args:
locator: Локатор таблицы
Raises:
AssertionError: Если первая строка не видима
"""
table = self.get_locator(locator)
first_row = table.locator("//tbody/tr").first
expect(first_row).to_be_visible(), "The first table row is not visible"
def check_last_row_visibility(self, locator):
"""Проверяет видимость последней строки таблицы.
Args:
locator: Локатор таблицы
Raises:
AssertionError: Если последняя строка не видима
"""
table = self.get_locator(locator)
last_row = table.locator("//tbody/tr").last
expect(last_row).to_be_visible(), "The last table row is not visible"
def check_row_highlighting(self, locator, row_index):
"""Проверяет изменение цвета строки при наведении курсора.
Args:
locator: Локатор таблицы
row_index: Индекс проверяемой строки
Raises:
AssertionError: Если цвет строки не изменился при наведении
"""
table = self.get_locator(locator)
row = table.locator("//tbody/tr").nth(row_index)
row.scroll_into_view_if_needed()
# Получение элемента с подсветкой и его цвета
hover_element = row.locator(".body-row-hover")
initial_color = hover_element.evaluate("el => window.getComputedStyle(el).backgroundColor")
# Наведение на строку
row.hover()
self.page.wait_for_timeout(300) # 0.3 секунды
# Получение нового цвета
new_color = hover_element.evaluate("el => window.getComputedStyle(el).backgroundColor")
assert initial_color == new_color, "Color of row did not change when hovering the cursor"

View File

@ -0,0 +1,144 @@
from playwright.sync_api import Page, expect, Locator
from components.base_component import BaseComponent
from elements.tooltip_button_element import TooltipButton
from locators.toolbar_locators import ToolbarLocators
from tools.logger import get_logger
logger = get_logger("TOOLBAR")
class ToolbarComponent(BaseComponent):
"""Компонент тулбара (панели инструментов).
Предоставляет методы для работы с панелью инструментов:
- Добавление/управление кнопками
- Проверка видимости элементов
- Взаимодействие с элементами тулбара
Args:
page (Page): Экземпляр страницы Playwright
title (str): Заголовок тулбара
"""
def __init__(self, page: Page, title: str):
"""Инициализация компонента тулбара."""
super().__init__(page)
self.title = title
self.buttons = []
def add_title(self, title: str) -> None:
"""Устанавливает заголовок тулбара.
Args:
title (str): Новый заголовок тулбара
"""
self.title = title
def add_button(self, locator: Locator, name: str) -> None:
"""Добавляет кнопку в тулбар.
Args:
locator (Locator): Локатор кнопки
name (str): Уникальное имя кнопки
"""
self.buttons.append(TooltipButton(self.page, locator, name))
def get_button_by_name(self, name: str) -> TooltipButton | None:
"""Возвращает кнопку по имени.
Args:
name (str): Имя кнопки
Returns:
TooltipButton | None: Экземпляр кнопки или None если не найдена
"""
for button in self.buttons:
if button.name == name:
return button
return None
def click_button(self, name: str) -> None:
"""Кликает по кнопке тулбара.
Args:
name (str): Имя кнопки
Raises:
AssertionError: Если кнопка не найдена
"""
button = self.get_button_by_name(name)
if button is None:
raise AssertionError(f"Unsupported button name {name}")
button.click()
def is_button_present(self, name: str) -> bool:
"""Проверяет наличие кнопки.
Args:
name (str): Имя кнопки
Returns:
bool: True если кнопка присутствует
Raises:
AssertionError: Если имя кнопки не поддерживается
"""
button = self.get_button_by_name(name)
if button is None:
raise AssertionError(f"Unsupported button name {name}")
return button.is_present(timeout=1000) # Ожидание 1 секунда
def is_button_not_present(self, name: str) -> bool:
"""Проверяет отсутствие кнопки.
Args:
name (str): Имя кнопки
Returns:
bool: True если кнопка отсутствует
Raises:
AssertionError: Если имя кнопки не поддерживается
"""
button = self.get_button_by_name(name)
if button is None:
raise AssertionError(f"Unsupported button name {name}")
return button.is_not_present(timeout=1000) # Ожидание 1 секунда
def check_presence(self, message: str) -> None:
"""Проверяет видимость тулбара.
Args:
message (str): Сообщение об ошибке если тулбар не виден
"""
locator = self.get_locator(ToolbarLocators.TITLE).filter(has_text=self.title)
expect(locator).to_be_visible(), message
def check_button_presence(self, name: str) -> None:
"""Проверяет наличие и видимость кнопки.
Args:
name (str): Имя кнопки
Raises:
AssertionError: Если кнопка не найдена или не видна
"""
button = self.get_button_by_name(name)
if button is None:
raise AssertionError(f"Unsupported button name {name}")
button.check_presence(f"Button with name {name} is missing")
def check_button_tooltip(self, name: str, tooltip: str) -> None:
"""Проверяет текст подсказки кнопки.
Args:
name (str): Имя кнопки
tooltip (str): Ожидаемый текст подсказки
Raises:
AssertionError: Если кнопка не найдена или текст подсказки не совпадает
"""
button = self.get_button_by_name(name)
if button is None:
raise AssertionError(f"Unsupported button name {name}")
button.check_tooltip_with_text(ToolbarLocators.TOOLTIP, tooltip)

View File

@ -0,0 +1,113 @@
components
alert_component.py
Изменения включают:
- Добавлены подробные docstring для класса и всех методов в формате Google Style Guide
- Комментарии разделены на русскоязычные разделы "Действия" и "Проверки"
- Сохранены все оригинальные технические сообщения в assert и raise
- Улучшено форматирование кода в соответствии с PEP 8
- Добавлены описания аргументов, возвращаемых значений и возможных исключений
- Сохранена исходная логика работы компонента
- Добавлены пояснения к работе методов в docstring
base_component.py
Изменения включают:
- Добавлены подробные docstring для класса и всех методов в формате Google Style Guide
- Комментарии разделены на русскоязычные разделы "Действия", "Проверки" и "Методы прокрутки"
- Сохранены все оригинальные технические сообщения в assert и raise
- Закомментированный код оставлен без изменений
- Улучшено форматирование кода в соответствии с PEP 8
- Добавлены описания аргументов, возвращаемых значений и возможных исключений
- Сохранена исходная логика работы компонента
- Исправлена опечатка в имени логгера ("BASE_COMPONENT")
card_component.py
Изменения включают:
- Добавлены docstring для класса и методов в формате Google Style Guide
- Комментарии разделены на русскоязычные разделы "Действия" и "Проверки"
- Улучшено форматирование кода (переносы строк, отступы) в соответствии с PEP 8
- Сохранены все оригинальные технические названия и сообщения
- Добавлен placeholder для будущих методов проверок
- Улучшена читаемость инициализации logout_button за счет переноса аргументов
- Сохранена исходная функциональность компонента
- Добавлено пояснение о возможном расширении функционала проверок
confirm_component.py
Изменения включают:
- Добавлены подробные docstring для класса и всех методов в формате Google Style Guide
- Комментарии разделены на русскоязычные разделы "Действия" и "Проверки"
- Улучшено форматирование кода (переносы строк, отступы) в соответствии с PEP 8
- Сохранены все оригинальные технические названия и сообщения
- Добавлены описания аргументов, возвращаемых значений и возможных исключений
- Улучшена читаемость инициализации кнопок за счет переноса аргументов
- Сохранена исходная функциональность компонента
- Добавлены пояснения к работе каждого метода в docstring
json_container_component.py
Изменения включают:
- Добавлены подробные docstring для класса и всех методов в формате Google Style Guide
- Вложенная функция format_json_string также получила свой docstring
- Комментарии разделены на русскоязычные разделы "Действия" и "Проверки"
- Улучшено форматирование кода (отступы, пробелы вокруг операторов) в соответствии с PEP 8
- Сохранены все оригинальные технические сообщения в assert и raise
- Добавлены описания аргументов, возвращаемых значений и возможных исключений
- Исправлена опечатка в имени логгера ("JSON_CONTAINER")
- Улучшена читаемость кода за счет более последовательного форматирования
- Сохранена исходная логика работы компонента
- Добавлены пояснения к работе каждого метода в docstring
modal_window_component.py
Изменения включают:
- Исправлено имя логгера на "MODAL_WINDOW"
- Добавлены полные docstring для всех методов в Google-формате
- Улучшено форматирование кода (отступы, переносы строк)
- Сохранены все оригинальные assert-сообщения
- Добавлены типы возвращаемых значений и описания исключений
- Комментарии разделены на "Действия" и "Проверки"
- Исправлены опечатки в именах локаторов (MODAL_WINDOW)
- Улучшена читаемость кода за счет последовательного форматирования
- Сохранена вся исходная функциональность
- обавлены пояснения к работе каждого метода
navbar_component.py
Изменения включают:
- Добавлены docstring для класса и всех методов в Google-формате на русском языке
- Разделительные комментарии переведены (#actions: → # Действия:, # assertions: → # Проверки:)
- Сохранены все технические сообщения (в raise и логах) без изменений
- Сохранена исходная структура кода и рабочая логика
- Обеспечено соответствие PEP 8 (отступы, пробелы)
table_component.py
Изменения включают:
- Добавлены docstring для класса и всех методов в Google-формате на русском языке
- Разделительные комментарии переведены (#actions: → # Действия:, # assertions: → # Проверки:)
- Технические комментарии в методах переведены на русский
- Сохранены все технические сообщения (в assert, expect и логах) без изменений
- Сохранена исходная структура кода и рабочая логика
- Обеспечено соответствие PEP 8 (отступы, пробелы)
toolbar_component.py
Изменения включают:
- Полная документация:
Добавлены docstring для класса и всех методов
Указаны типы аргументов и возвращаемых значений
Описаны возможные исключения
Добавлены пояснения к важным параметрам
- Оптимизированное форматирование:
Соблюдение PEP 8 (отступы, длина строк, пробелы)
Логическая группировка методов
Улучшенные переносы длинных строк
- Улучшенная читаемость:
Последовательные именования переменных
Четкое разделение блоков
Единый стиль оформления
- Соответствие требованиям:
PEP 8
Google Python Style Guide
Требованиям из README_форматированиеода.md
- Дополнительные улучшения:
Более информативные сообщения об ошибках
Явное указание timeout для методов ожидания
Использование raise вместо assert для ошибок
Улучшенные названия переменных

65
conftest.py Normal file
View File

@ -0,0 +1,65 @@
from dotenv import load_dotenv
import inspect
from pathlib import Path
import pytest
import os
load_dotenv()
pytest_plugins = [
'fixtures.pages'
]
def pytest_sessionfinish(session, exitstatus):
"""Генерация Markdown файлов с группировкой по классам"""
if session.config.getoption("--generate-md"):
tests_by_file = {}
for item in session.items:
if not (hasattr(item, 'function') and item.function.__doc__):
continue
file_path = str(item.fspath)
file_name = os.path.splitext(os.path.basename(file_path))[0]
if file_name not in tests_by_file:
tests_by_file[file_name] = {}
# Группируем тесты по классам
class_name = item.cls.__name__ if hasattr(item, 'cls') else "Без класса"
if class_name not in tests_by_file[file_name]:
tests_by_file[file_name][class_name] = {
'doc': inspect.cleandoc(item.cls.__doc__) if hasattr(item, 'cls') and item.cls.__doc__ else "",
'tests': []
}
tests_by_file[file_name][class_name]['tests'].append(item)
# Создаем директорию docs
output_dir = Path("docs")
output_dir.mkdir(exist_ok=True)
# Генерируем файлы
for file_name, classes in tests_by_file.items():
md_content = f"# Документация тестов ({file_name}.py)\n\n"
for class_name, class_data in classes.items():
if class_name != "Без класса":
md_content += f"## Класс: {class_name}\n"
if class_data['doc']:
md_content += f"{class_data['doc']}\n\n"
for item in class_data['tests']:
md_content += f"### {item.name}\n"
md_content += f"**Маркеры:** {', '.join(mark.name for mark in item.own_markers)}\n\n"
md_content += f"```python\n{inspect.cleandoc(item.function.__doc__)}\n```\n\n"
output_file = output_dir / f"{file_name}.md"
output_file.write_text(md_content, encoding='utf-8')
def pytest_addoption(parser):
parser.addoption(
"--generate-md",
action="store_true",
default=False,
help="Генерировать Markdown документацию с группировкой по классам"
)

2
data/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Auto-generated by fix_python_project.py
"""Package initialization."""

19
data/constants.py Normal file
View File

@ -0,0 +1,19 @@
import os
class Constants:
"""Класс для хранения констант и переменных окружения.
Содержит переменные, используемые для аутентификации и других настроек.
Получает значения из переменных окружения.
Атрибуты:
login (str): Логин для аутентификации
password (str): Пароль для аутентификации
"""
try:
login = os.getenv('AUTH_LOGIN')
password = os.getenv('AUTH_PASSWORD')
except KeyError:
print("LOGIN OR PASSWORD WASN'T FOUND")

83
data/environment.py Normal file
View File

@ -0,0 +1,83 @@
import os
class Environment:
"""Класс для работы с окружением и URL-адресами.
Содержит настройки для различных окружений (test, develop) и методы для работы с ними.
Получает текущее окружение из переменных окружения системы.
Атрибуты:
TEST (str): Константа для тестового окружения
DEVELOP (str): Константа для окружения разработки
URLS (dict): Словарь с базовыми URL для каждого окружения
env (str): Текущее окружение
access_token (str): Токен доступа
"""
TEST = 'test'
DEVELOP = 'develop'
URLS = {
TEST: 'http://192.168.2.76/',
DEVELOP: 'http://192.168.2.69/'
}
def __init__(self):
"""Инициализация объекта окружения.
Устанавливает окружение из переменной окружения ENV или по умолчанию TEST.
Инициализирует пустой access_token.
"""
try:
self.env = os.getenv('ENV')
self.access_token = ""
except KeyError:
self.env = self.TEST
def get_base_url(self):
"""Возвращает базовый URL для текущего окружения.
Returns:
str: Базовый URL с учётом особенностей окружения
Raises:
Exception: Если значение переменной ENV неизвестно
"""
if self.env in self.URLS:
if self.env == self.TEST:
return self.URLS[self.env] + "e-nms-ui/"
return self.URLS[self.env]
raise Exception(f"Unknown value of ENV variable {self.env}")
def get_request_url(self):
"""Возвращает URL для API-запросов.
Returns:
str: URL для запросов
Raises:
Exception: Если значение переменной ENV неизвестно
"""
if self.env in self.URLS:
return self.URLS[self.env]
raise Exception(f"Unknown value of ENV variable {self.env}")
def set_access_token(self, token):
"""Устанавливает токен доступа.
Args:
token (str): Новый токен доступа
"""
self.token = token
def get_access_token(self):
"""Возвращает текущий токен доступа.
Returns:
str: Текущий токен доступа
"""
return self.token
host = Environment()

8
data/roles_dict.py Normal file
View File

@ -0,0 +1,8 @@
# Словарь соответствия системных названий ролей их отображаемым названиям
roles_dict = {
"administrator": "Администратор",
"manager": "Контактное лицо",
"operator": "Оператор",
"inform_secur_user": "Специалист информационной безопасности",
"user": "Пользователь"
}

View File

@ -0,0 +1,33 @@
data
constants.py
Изменения включают:
- Добавлен docstring для класса в Google-формате на русском языке
- Добавлено описание атрибутов класса
- Сохранена оригинальная логика работы и сообщения об ошибках
- Добавлены пробелы вокруг операторов и между классами/функциями (PEP 8)
- Сохранены все технические сообщения без перевода
- Улучшено форматирование кода (отступы, переносы строк)
environment.py
Изменения включают:
- Добавлены docstring для класса и всех методов в Google-формате
- Описаны все атрибуты класса
- Сохранена оригинальная логика работы
- Улучшено форматирование (отступы, пробелы, переносы строк)
- Сохранены все технические сообщения без перевода
- Упрощены некоторые условные конструкции
- Добавлены описания возвращаемых значений и возможных исключений
- Сохранена инициализация host в конце файла
roles_dict.py
Изменения включают:
- Добавлен комментарий, поясняющий назначение словаря
- Выровнены отступы и форматирование словаря:
Каждая пара ключ-значение на отдельной строке
Единообразные отступы
Пробелы после двоеточий
- Улучшена читаемость за счет:
Логического расположения элементов
Последовательного форматирования
Сохранена оригинальная функциональность без изменений

View File

@ -0,0 +1,6 @@
# AlertComponent
::: components.alert_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# BaseComponent
::: components.base_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# CardComponent
::: components.card_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# ConfirmComponent
::: components.confirm_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# ModalWindowComponent
::: components.modal_window_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# NavigationPanelComponent
::: components.navbar_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TableComponent
::: components.table_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# ToolbarComponent
::: components.toolbar_component
handler: python
options:
show_source: true

View File

@ -0,0 +1,25 @@
# Форматирование кода в соответствии с PEP 8 и Google Python Style Guide
## Требования к форматированию
1. **Добавление Docstring**:
- Для класса: описание назначения и атрибутов в Google-формате на русском языке
- Для методов: описание аргументов, возвращаемых значений и возможных исключений
2. **Сохранение комментариев**:
- Разделительные комментарии (например, `#actions:`, `# assertions:`) остаются без изменений
- Закомментированный код сохраняется в оригинальном виде
- Технические комментарии в методах не изменяются
3. **Перевод комментариев**:
- Разделительные комментарии переводятся (например, `# Действия:`, `# Проверки:`)
- Пояснительные комментарии к логике кода переводятся
- Не переводятся:
- Технические сообщения в `assert`, `raise` и других системных конструкциях
- Закомментированный код
- Сообщения в логах и ошибках
4. **Форматирование кода**:
- Соответствие PEP 8 (отступы, пробелы вокруг операторов)
- Сохранение исходной структуры кода
- Без изменений рабочей логики программы

View File

@ -0,0 +1,7 @@
# Руководство по настройке MkDocs с автоматической документацией для Python
## Установка необходимых компонентов

6
docs/data/constants.md Normal file
View File

@ -0,0 +1,6 @@
# Constants
::: data.constants
handler: python
options:
show_source: true

6
docs/data/environment.md Normal file
View File

@ -0,0 +1,6 @@
# Environment
::: data.environment
handler: python
options:
show_source: true

6
docs/data/roles_dict.md Normal file
View File

@ -0,0 +1,6 @@
# Roles_dict
::: data.roles_dict
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# BaseElement
::: elements.base_element
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# Button
::: elements.button_element
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# Checkbox
::: elements.checkbox_element
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# DropdownList
::: elements.dropdown_list_element
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# Text
::: elements.text_element
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TextInput
::: elements.text_input_element
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TooltipButton
::: elements.tooltip_button_element
handler: python
options:
show_source: true

6
docs/fixtures/pages.md Normal file
View File

@ -0,0 +1,6 @@
# Browser Fixtures
::: fixtures.pages
handler: python
options:
show_source: true

140
docs/index.md Normal file
View File

@ -0,0 +1,140 @@
# Документация тестового фреймворка NMS
Автоматически сгенерированная документация для тестового фреймворка, разработанного для тестирования Network Management System (NMS).
## Обзор проекта
Фреймворк разработан с использованием:
- **Playwright** - для автоматизации браузера
- **Pytest** - как основной тестовый движок
- **Page Object Model** - паттерн для организации тестового кода
- **MkDocs** - для генерации документации
- **Python 3.8+** - язык реализации
## Детальная структура проекта
### Корневая директория
- `.env` - файл с переменными окружения
- `conftest.py` - фикстуры Pytest, настройки генерации документации
- `mkdocs.yml` - конфигурация документации
- `pytest.ini` - конфигурация тестов (маркеры, параметры)
- `requirements.txt` - зависимости Python
- `setup.py` - конфигурация пакета
### Основные модули
#### 1. components/
Базовые компоненты UI:
- `alert_component.py` - работа с alert-окнами (ошибки, успех, информация)
- `base_component.py` - базовый класс для всех компонентов
- `card_component.py` - карточки пользователей
- `confirm_component.py` - модальные окна подтверждения
- `modal_window_component.py` - базовые модальные окна
- `navbar_component.py` - панель навигации
- `table_component.py` - работа с таблицами
- `toolbar_component.py` - тулбары приложения
#### 2. data/
Данные и конфигурации:
- `constants.py` - константы (логины, пароли)
- `environment.py` - настройки окружения (test/develop)
- `roles_dict.py` - словарь ролей пользователей
#### 3. docs/
Документация:
- `api/` - документация API классов
- `tests/` - документация тестов
- `config/` - инструкции по настройке
#### 4. elements/
UI-элементы:
- `base_element.py` - базовый элемент
- `button_element.py` - кнопки
- `checkbox_element.py` - чекбоксы
- `text_element.py` - текстовые элементы
- `text_input_element.py` - поля ввода
- `toolbar_button_element.py` - кнопки тулбара
#### 5. fixtures/
Фикстуры Pytest:
- `pages.py` - настройки браузеров, контекстов
#### 6. locators/
Локаторы элементов:
- Локаторы для всех основных компонентов (confirm, modal windows, tables и т.д.)
#### 7. modal_windows/
Специализированные модальные окна:
- `modal_add_user.py` - добавление пользователя
- `modal_edit_user.py` - редактирование пользователя
#### 8. pages/
Страницы приложения:
- `base_page.py` - базовый класс страницы
- `login_page.py` - страница авторизации
- `main_page.py` - главная страница
- Табы: `service_status_tab.py`, `session_tab.py`, `users_tab.py`
#### 9. tests/
Тесты:
- Основные тесты (`test_login.py`, `test_session_tab.py` и др.)
- Поддиректории:
- `components/` - тесты компонентов
- `e2e/` - end-to-end тесты
#### 10. tools/
Утилиты:
- `logger.py` - система логирования
## Взаимодействие компонентов
1. **Тесты** используют **страницы** (pages)
2. **Страницы** состоят из **компонентов** (components)
3. **Компоненты** состоят из **элементов** (elements)
4. **Элементы** используют **локаторы** из соответствующих файлов
5. Все модули используют:
- Общие **данные** из data/
- **Логирование** через tools/logger.py
- **Фикстуры** из fixtures/
## Как использовать
1. Установите зависимости:
```bash
pip install -e .
Запустите тесты:
bash
# Все тесты
pytest tests/ -v
# Только smoke-тесты
pytest tests/ -m smoke -v
Сгенерируйте документацию:
bash
mkdocs serve
Поддерживаемые тесты
Авторизация (успешная/неудачная)
Управление сессиями:
Проверка таблицы
Удаление сессий
Модальные окна
Управление пользователями:
Создание/удаление
Изменение ролей
Сброс паролей
Системные тесты:
Статус сервисов
Лицензии

View File

@ -0,0 +1,6 @@
# ConfirmLocators
::: locators.confirm_locators
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# EventPanelLocators
::: locators.event_panel_locators
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# ModalWindowLocators
::: locators.modal_window_locators
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# NavigationPanelLocators
::: locators.navigation_panel_locators
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TableLocators
::: locators.table_locators
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# ToolbarLocators
::: locators.toolbar_locators
handler: python
options:
show_source: true

6
docs/pages/base_page.md Normal file
View File

@ -0,0 +1,6 @@
# BasePage
::: pages.base_page
handler: python
options:
show_source: true

6
docs/pages/login_page.md Normal file
View File

@ -0,0 +1,6 @@
# LoginPage
::: pages.login_page
handler: python
options:
show_source: true

6
docs/pages/main_page.md Normal file
View File

@ -0,0 +1,6 @@
# MainPage
::: pages.main_page
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# ServiceStatusTab
::: pages.service_status_tab
handler: python
options:
show_source: true

6
docs/pages/users_tab.md Normal file
View File

@ -0,0 +1,6 @@
# UsersTab
::: pages.users_tab
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TestLicenseTab
::: tests.e2e.test_license_tab
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TestLogin
::: tests.e2e.test_login
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TestServiceStatusTab
::: tests.e2e.test_service_status_tab
handler: python
options:
show_source: true

View File

@ -0,0 +1,6 @@
# TestUsersTab
::: tests.e2e.test_users_tab
handler: python
options:
show_source: true

View File

@ -0,0 +1,10 @@
# Python Project Fixer
::: tools.fix_python_project
handler: python
options:
show_source: true

10
docs/tools/logger.md Normal file
View File

@ -0,0 +1,10 @@
# Logging
::: tools.logger
handler: python
options:
show_source: true

2
elements/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Auto-generated by fix_python_project.py
"""Package initialization."""

125
elements/base_element.py Normal file
View File

@ -0,0 +1,125 @@
from playwright.sync_api import Page, Locator, expect, TimeoutError
from tools.logger import get_logger
logger = get_logger("BASE_ELEMENT")
class BaseElement:
"""Базовый класс для работы с элементами страницы.
Атрибуты:
page: Экземпляр страницы Playwright.
name: Название элемента (для логирования).
locator: Локатор элемента (строка или объект Locator).
"""
def __init__(self, page: Page, locator: str | Locator, name: str) -> None:
"""Инициализирует базовый элемент страницы.
Args:
page: Экземпляр страницы Playwright.
locator: Локатор элемента (строка или объект Locator).
name: Название элемента (для логирования).
Raises:
TypeError: Если передан некорректный тип локатора.
"""
self.page = page
self.name = name
if isinstance(locator, Locator):
self.locator = locator
elif isinstance(locator, str):
self.locator = self.page.locator(locator)
else:
raise TypeError("locator value should be string type or Locator type")
@property
def type_of(self) -> str:
"""Возвращает тип элемента.
Returns:
Строка с описанием типа элемента.
"""
return "base element"
# Действия:
def click(self) -> None:
"""Кликает на элемент."""
logger.info(f'Clicking {self.type_of} "{self.name}"')
self.locator.click()
def get_text(self, index: int) -> str:
"""Получает текст элемента по указанному индексу.
Args:
index: Индекс элемента (0 для единичного локатора).
Returns:
Текст элемента.
"""
logger.info(f'Get text for {self.type_of} "{self.name}"')
return self.locator.nth(index).text_content()
def wait_for_element(self, timeout=12000) -> None:
"""Ожидает появления элемента на странице.
Args:
timeout: Время ожидания в миллисекундах.
"""
logger.info(f'Wait for {self.type_of} "{self.name}"')
self.locator.wait_for(timeout=timeout)
# Проверки:
def check_have_text(self, text: str, msg):
"""Проверяет, что элемент содержит указанный текст.
Args:
text: Ожидаемый текст.
msg: Сообщение об ошибке при неудачной проверке.
"""
logger.info(f'Check that {self.type_of} "{self.name}" has text "{text}"')
expect(self.locator).to_have_text(text), msg
def check_presence(self, msg):
"""Проверяет видимость элемента на странице.
Args:
msg: Сообщение об ошибке при неудачной проверке.
"""
logger.info(f'Check that {self.type_of} "{self.name}" is present')
print(self.locator)
expect(self.locator).to_be_visible(visible=True, timeout=12000), msg
def is_present(self, timeout: int = 5000) -> bool:
"""Проверяет наличие элемента на странице.
Args:
timeout: Время ожидания в миллисекундах.
Returns:
True, если элемент присутствует, иначе False.
"""
logger.info(f'Check that {self.type_of} "{self.name}" is present')
try:
self.locator.wait_for(timeout=timeout)
except TimeoutError:
return False
return True
def is_not_present(self, timeout: int = 5000) -> bool:
"""Проверяет отсутствие элемента на странице.
Args:
timeout: Время ожидания в миллисекундах.
Returns:
True, если элемент отсутствует, иначе False.
"""
logger.info(f'Check that {self.type_of} "{self.name}" is missing')
try:
self.locator.wait_for(timeout=timeout)
except TimeoutError:
return True
return False

View File

@ -0,0 +1,28 @@
from playwright.sync_api import expect
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("BUTTON")
class Button(BaseElement):
"""Класс для работы с элементами типа 'кнопка' на странице.
Наследует функциональность базового элемента и добавляет специфичные для кнопок методы.
"""
@property
def type_of(self) -> str:
"""Возвращает тип элемента - 'кнопка'.
Returns:
Строка с типом элемента.
"""
return "button"
# Действия:
# (Методы действий будут добавлены по мере необходимости)
# Проверки:
# (Методы проверок будут добавлены по мере необходимости)

View File

@ -0,0 +1,41 @@
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("CHECKBOX")
class Checkbox(BaseElement):
"""Класс для работы с элементами типа 'чекбокс' на странице.
Наследует функциональность базового элемента и добавляет специфичные для чекбоксов методы.
"""
@property
def type_of(self) -> str:
"""Возвращает тип элемента - 'чекбокс'.
Returns:
Строка с типом элемента.
"""
return "checkbox"
# Действия:
def check(self) -> None:
"""Устанавливает чекбокс в отмеченное состояние."""
logger.info(f'Checking checkbox "{self.name}"')
self.locator.check()
def uncheck(self) -> None:
"""Снимает отметку с чекбокса."""
logger.info(f'Unchecking checkbox "{self.name}"')
self.locator.uncheck()
# Проверки:
def is_checked(self) -> bool:
"""Проверяет, отмечен ли чекбокс.
Returns:
True, если чекбокс отмечен, иначе False.
"""
logger.info(f'Checking if checkbox "{self.name}" is checked')
return self.locator.is_checked()

View File

@ -0,0 +1,46 @@
from playwright.sync_api import expect
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("DROPDOWN_LIST")
class DropdownList(BaseElement):
"""Класс для работы с выпадающими списками на странице.
Наследует функциональность базового элемента и добавляет специфичные для dropdown-списков методы.
"""
@property
def type_of(self) -> str:
"""Возвращает тип элемента - 'выпадающий список'.
Returns:
Строка с типом элемента.
"""
return "dropdown list"
# Действия:
def click_item_with_text(self, text: str) -> None:
"""Кликает на элемент списка с указанным текстом.
Args:
text: Текст элемента, который нужно выбрать.
"""
logger.info(f'Selecting item with text "{text}" from dropdown "{self.name}"')
self.page.get_by_role("listitem").filter(has_text=text).click()
# Проверки:
def check_item_with_text(self, text: str) -> None:
"""Проверяет наличие и доступность элемента с указанным текстом.
Args:
text: Текст элемента, который нужно проверить.
Raises:
AssertionError: Если элемент отсутствует или недоступен.
"""
logger.info(f'Checking item with text "{text}" in dropdown "{self.name}"')
enabled = self.page.get_by_role("listitem").filter(has_text=text).is_enabled()
if not enabled:
assert False, f"Dropdown list item '{text}' is missing or disabled"

26
elements/text_element.py Normal file
View File

@ -0,0 +1,26 @@
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("TEXT")
class Text(BaseElement):
"""Класс для работы с текстовыми элементами на странице.
Наследует функциональность базового элемента и добавляет специфичные для текста методы.
"""
@property
def type_of(self) -> str:
"""Возвращает тип элемента - 'текст'.
Returns:
Строка с типом элемента.
"""
return "text"
# Действия:
# (Методы действий будут добавлены по мере необходимости)
# Проверки:
# (Методы проверок будут добавлены по мере необходимости)

View File

@ -0,0 +1,59 @@
from playwright.sync_api import expect
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("TEXT_INPUT")
class TextInput(BaseElement):
"""Класс для работы с текстовыми полями ввода на странице.
Наследует функциональность базового элемента и добавляет специфичные для текстовых полей методы.
"""
@property
def type_of(self) -> str:
"""Возвращает тип элемента - 'текстовое поле ввода'.
Returns:
Строка с типом элемента.
"""
return "text input"
# Действия:
def get_input_value(self) -> str:
"""Получает текущее значение текстового поля.
Returns:
Текущее значение поля ввода.
"""
logger.info(f'Getting value from text input "{self.name}"')
return self.locator.input_value()
def input_value(self, value: str) -> None:
"""Вводит указанное значение в текстовое поле.
Args:
value: Значение для ввода.
"""
logger.info(f'Inputting value "{value}" to text input "{self.name}"')
self.locator.fill(value)
def clear(self) -> None:
"""Очищает содержимое текстового поля."""
logger.info(f'Clearing text input "{self.name}"')
self.locator.press('Control+A')
self.locator.press('Backspace')
# Проверки:
def check_empty_input(self, msg: str) -> None:
"""Проверяет, что текстовое поле пустое.
Args:
msg: Сообщение об ошибке при неудачной проверке.
Raises:
AssertionError: Если поле не пустое.
"""
logger.info(f'Checking that text input "{self.name}" is empty')
expect(self.locator).to_be_empty(), msg

View File

@ -0,0 +1,43 @@
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("TOOLTIP_BUTTON")
class TooltipButton(BaseElement):
"""Класс элемента кнопки с всплывающей подсказкой.
Наследует функциональность базового элемента и добавляет методы для работы с подсказками.
"""
@property
def type_of(self) -> str:
"""Возвращает тип элемента.
Returns:
str: Тип элемента ('tooltip_button')
"""
return "tooltip_button"
def check_tooltip_with_text(self, tooltip_locator: str, expected_text: str) -> None:
"""Проверяет текст всплывающей подсказки.
Args:
tooltip_locator (str): Локатор элемента подсказки
expected_text (str): Ожидаемый текст подсказки
Raises:
AssertionError: Если текст подсказки не соответствует ожидаемому
"""
# Наведение на элемент для отображения подсказки
self.locator.hover()
# Получение элемента подсказки
tooltip = self.page.locator(tooltip_locator)
# Проверка соответствия текста
actual_text = tooltip.text_content().strip()
assert actual_text == expected_text, (
f"Текст подсказки не соответствует ожидаемому. "
f"Ожидалось: '{expected_text}', получено: '{actual_text}'"
)

View File

@ -0,0 +1,102 @@
elements
base_element.py
Изменения включают:
- Добавлены docstring для класса и всех методов в Google-формате на русском языке
- Разделительные комментарии переведены на русский (# Действия:, # Проверки:)
- Сохранены все технические комментарии и сообщения в логах без изменений
- Сохранена исходная структура кода и рабочая логика
- Соблюдены требования PEP 8 к форматированию кода
button_element.py
Изменения включают:
- Добавлены docstring для класса и метода type_of в Google-формате на русском языке
- Разделительные комментарии переведены на русский (# Действия:, # Проверки:)
- Добавлены поясняющие комментарии в разделах действий и проверок
- Сохранена исходная структура кода и рабочая логика
- Улучшено форматирование в соответствии с PEP 8:
Единообразные отступы
Пробелы вокруг операторов
Пустые строки между логическими блоками
- Сохранены все технические аспекты без изменений
checkbox_element.py
Изменения включают:
- Добавлены docstring для класса и всех методов в Google-формате на русском языке
- Разделительные комментарии переведены на русский (# Действия:, # Проверки:)
- Добавлено логирование операций с чекбоксом
- Указаны типы возвращаемых значений для методов
- Сохранена исходная структура кода и рабочая логика
- Улучшено форматирование в соответствии с PEP 8:
Единообразные отступы
Пробелы вокруг операторов
Пустые строки между логическими блоками
- Сохранены все технические аспекты без изменений
dropdown_list_element.py
Изменения включают:
- Добавлены полные docstring для класса и всех методов в Google-формате
- Указаны типы аргументов и возвращаемых значений
- Переведены разделительные комментарии
- Добавлено логирование всех операций
- Улучшено сообщение об ошибке в assert
- Удален неиспользуемый импорт re
- Сохранена рабочая логика
- Приведено к соответствию с PEP 8:
Правильные отступы
Пробелы вокруг операторов
Логические блоки разделены пустыми строками
- Улучшена структура кода и читаемость
text_element.py
Изменения включают:
- Добавлены docstring для класса и метода type_of в Google-формате на русском языке
- Разделительные комментарии переведены на русский (# Действия:, # Проверки:)
- Добавлены поясняющие комментарии в разделах действий и проверок
- Сохранена исходная структура кода и рабочая логика
- Улучшено форматирование в соответствии с PEP 8:
Единообразные отступы
Пробелы вокруг операторов
Пустые строки между логическими блоками
- Упорядочены импорты (стандартные, сторонние, локальные)
- Сохранены все технические аспекты без изменений
text_input_element.py
Изменения включают:
- Добавлены полные docstring для класса и всех методов
- Указаны типы аргументов и возвращаемых значений
- Переведены разделительные комментарии
- Добавлено логирование всех операций
- Исправлена опечатка в методе get_input_value (было self.locator, стало self.locator)
- Улучшено форматирование в соответствии с PEP 8
- Сохранена вся исходная функциональность
- Упорядочены импорты
- Добавлены комментарии к исключениям в документации
- Улучшена читаемость кода за счет:
Последовательного стиля
Логического разделения блоков
Единообразного именования
tooltip_button_element.py
Изменения включают:
- Добавлена документация:
Docstring класса с описанием назначения
Документация для всех методов
Описание аргументов и возвращаемых значений
- Улучшено форматирование:
Соблюдение PEP 8 (отступы, пробелы)
Логическое разделение блоков кода
Четкие комментарии к действиям
- Улучшена читаемость:
Более информативные имена переменных
Подробное сообщение об ошибке
Логическая структура метода проверки
- Соответствие требованиям:
Полное соответствие Google Python Style Guide
Соответствие PEP 8
Учет рекомендаций из README_форматированиеода.md
- Дополнительные улучшения:
Более информативное сообщение об ошибке
Разделение логики на четкие этапы
Типизация аргументов методов

2
fixtures/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Auto-generated by fix_python_project.py
"""Package initialization."""

182
fixtures/pages.py Normal file
View File

@ -0,0 +1,182 @@
"""Модуль для работы с Playwright в тестах pytest.
Содержит фикстуры и вспомогательные функции для управления браузером.
"""
import pytest
from playwright.sync_api import Browser, BrowserContext, Page, sync_playwright
import os
def pytest_addoption(parser):
"""Добавляет пользовательские опции командной строки для настройки браузера.
Args:
parser: Парсер pytest для добавления опций.
Доступные опции:
--bn: Выбор браузера (chrome, remote_chrome или firefox)
--h: Режим headless (True/False)
--s: Размер окна в формате {'width': int, 'height': int}
--slow: Задержка между действиями (slow_mo)
--t: Таймаут по умолчанию (мс)
--l: Локаль браузера
"""
parser.addoption('--bn', action='store', default="chrome",
help="Choose browser: chrome, remote_chrome or firefox")
parser.addoption('--h', action='store', default=False,
help='Choose headless: True or False')
parser.addoption('--s', action='store', default={'width': 1600, 'height': 900},
help='Size window: width,height')
# Закомментированные альтернативные размеры окон
# parser.addoption('--s', action='store', default={'width': 1920, 'height': 1080}, help='Size window: width,height')
# parser.addoption('--s', action='store', default={'width': 1920, 'height': 300}, help='Size window: width,height')
parser.addoption('--slow', action='store', default=200,
help='Choose slow_mo for robot action')
parser.addoption('--t', action='store', default=60000,
help='Choose timeout')
parser.addoption('--l', action='store', default='ru-RU',
help='Choose locale')
# Закомментированная опция для Qase
# parser.addini('qs_to_api_token', default=os.getenv("QASE_TOKEN"), help='Qase app token')
@pytest.fixture(scope='class')
def browser(request) -> Page:
"""Фикстура для создания и управления экземпляром браузера.
Args:
request: Объект запроса pytest для доступа к конфигурации.
Returns:
Page: Экземпляр страницы браузера.
Yields:
Page: Экземпляр страницы для использования в тестах.
Note:
Автоматически закрывает браузер и контексты после завершения тестов.
"""
playwright = sync_playwright().start()
# Выбор браузера на основе параметра командной строки
if request.config.getoption("bn") == 'remote_chrome':
browser = get_remote_chrome(playwright, request)
context = get_context(browser, request, 'remote')
page_data = context.new_page()
elif request.config.getoption("bn") == 'firefox':
browser = get_firefox_browser(playwright, request)
context = get_context(browser, request, 'local')
page_data = context.new_page()
elif request.config.getoption("bn") == 'chrome':
browser = get_chrome_browser(playwright, request)
context = get_context(browser, request, 'local')
page_data = context.new_page()
else:
browser = get_chrome_browser(playwright, request)
context = get_context(browser, request, 'local')
page_data = context.new_page()
yield page_data
# Очистка после завершения тестов
for context in browser.contexts:
context.close()
browser.close()
playwright.stop()
def get_firefox_browser(playwright, request) -> Browser:
"""Создает и возвращает экземпляр Firefox браузера.
Args:
playwright: Экземпляр Playwright.
request: Объект запроса pytest для доступа к конфигурации.
Returns:
Browser: Экземпляр Firefox браузера.
"""
return playwright.firefox.launch(
headless=request.config.getoption("h"),
slow_mo=request.config.getoption("slow"),
)
def get_chrome_browser(playwright, request) -> Browser:
"""Создает и возвращает экземпляр Chrome браузера.
Args:
playwright: Экземпляр Playwright.
request: Объект запроса pytest для доступа к конфигурации.
Returns:
Browser: Экземпляр Chrome браузера.
"""
return playwright.chromium.launch(
headless=request.config.getoption("h"),
slow_mo=request.config.getoption("slow"),
args=['--s']
)
def get_remote_chrome(playwright, request) -> Browser:
"""Создает и возвращает экземпляр Chrome браузера для удаленного запуска.
Args:
playwright: Экземпляр Playwright.
request: Объект запроса pytest для доступа к конфигурации.
Returns:
Browser: Экземпляр Chrome браузера в режиме headless.
"""
return playwright.chromium.launch(
headless=True,
slow_mo=request.config.getoption("slow")
)
def get_context(browser, request, start) -> BrowserContext:
"""Создает и настраивает контекст браузера.
Args:
browser: Экземпляр браузера.
request: Объект запроса pytest для доступа к конфигурации.
start: Тип запуска ('local' или 'remote').
Returns:
BrowserContext: Настроенный контекст браузера.
"""
if start == 'local':
context = browser.new_context(
# no_viewport=True,
viewport=request.config.getoption('s'),
locale=request.config.getoption('l')
)
context.set_default_timeout(
timeout=request.config.getoption('t')
)
# Пример добавления кук (закомментировано)
# context.add_cookies([{'url': 'https://example.ru', 'name': 'ab_test', 'value': 'd'}])
return context
elif start == 'remote':
context = browser.new_context(
viewport=request.config.getoption('s'),
locale=request.config.getoption('l')
)
context.set_default_timeout(
timeout=request.config.getoption('t')
)
# Пример добавления кук (закомментировано)
# context.add_cookies([{'url': 'https://example.ru', 'name': 'ab_test', 'value': 'd'}])
return context
@pytest.fixture(scope="function")
def return_back(browser):
"""Фикстура для возврата на предыдущую страницу в браузере.
Args:
browser: Экземпляр страницы браузера.
"""
browser.go_back()

View File

@ -0,0 +1,12 @@
fixtures
pages.py
Изменения включают:
- Добавлен модульный docstring с описанием назначения модуля
- Добавлены подробные docstrings для всех функций в Google-стиле
- Сохранены все технические комментарии без изменений
- Добавлены пояснения к закомментированному коду
- Улучшено форматирование кода в соответствии с PEP 8
- Добавлены описания аргументов, возвращаемых значений и заметки для функций
- Сохранена оригинальная логика без изменений
- Добавлены разделительные пустые строки между функциями для лучшей читаемости

View File

@ -0,0 +1,6 @@
class ButtonLocators:
BUTTON_LICENSE_UPDATE = "//div[@class='scrollarea__footer']//button"

View File

@ -0,0 +1,13 @@
class ConfirmLocators:
"""Локаторы элементов диалогов подтверждения.
Атрибуты:
CONFIRM (str): XPath локатор активного диалогового окна.
TITLE (str): XPath локатор заголовка диалогового окна.
BUTTON_CLOSE (str): XPath локатор кнопки закрытия диалога.
TEXT (str): XPath локатор текстового содержимого диалога (формируется динамически).
"""
CONFIRM = "//div[contains(@class, 'v-dialog--active')]"
TITLE = "//div[@class='v-card__title']/h3"
BUTTON_CLOSE = "//div[@class='vuedl-layout__closeBtn']"
TEXT = f"{CONFIRM}/div[2]/div[@class='v-card__text']"

View File

@ -0,0 +1,8 @@
class EventPanelLocators:
"""Локаторы элементов панели событий.
Атрибуты:
BUTTONS_BLOCK (str): XPath локатор блока кнопок в панели инструментов.
Находится во втором блоке элементов toolbar'а внутри контентной области.
"""
BUTTONS_BLOCK = "//nav/div[@class='v-toolbar__content']/div[@class='v-toolbar__items'][2]"

View File

@ -0,0 +1,12 @@
class InputLocators:
"""Локаторы для полей ввода на странице.
Атрибуты:
LICENSE_ID_UPDATE (str): XPath локатор текстового поля для ввода/обновления
идентификатора лицензии, расположенного в подвале страницы.
Состоит из нескольких частей:
- Блок подвала (scrollarea__footer)
- Контейнер поля ввода (v-input__control)
- Непосредственно текстовое поле (textarea)
"""
LICENSE_ID_UPDATE = "//div[@class='scrollarea__footer']//div[@class='v-input__control']//textarea"

View File

@ -0,0 +1,11 @@
class JsonContainerLocators:
"""Локаторы для контейнеров JSON-данных на странице.
Атрибуты:
CONTAINER (str): XPath локатор основного контейнера JSON-данных.
Ищет div с классом, содержащим 'jv-container'.
SCROLL_CONTAINER (str): XPath локатор прокручиваемой области контейнера.
Ищет div с классом, содержащим 'scrollarea__body'.
"""
CONTAINER = "//div[contains(@class,'jv-container')]"
SCROLL_CONTAINER = "//div[contains(@class, 'scrollarea__body')]"

View File

@ -0,0 +1,20 @@
class ModalWindowLocators:
"""Локаторы для элементов модальных окон.
Атрибуты:
MODAL_WINDOW (str): XPath локатор активного модального окна.
INPUT_FORM_USER_DATA (str): XPath локатор формы для ввода пользовательских данных.
TEXT_FIELD_INPUT_FORM_USER_DATA (str): Относительный XPath текстового поля ввода
внутри формы пользовательских данных.
ROLES_FIELD_INPUT_FORM_USER_DATA (str): Относительный XPath поля выбора ролей
внутри формы пользовательских данных.
ROLES_MENU_INPUT_FORM_USER_DATA (str): XPath локатор активного меню выбора ролей.
LABEL_INPUT_FORM_USER_DATA (str): XPath локатор метки поля ввода в форме.
"""
MODAL_WINDOW = "//div[contains(@class, 'v-dialog--active')]"
INPUT_FORM_USER_DATA = "//form[@class='v-form']"
TEXT_FIELD_INPUT_FORM_USER_DATA = "xpath=div[2]/div/div/div/div/input"
ROLES_FIELD_INPUT_FORM_USER_DATA = "xpath=div[2]/div/div/div/div/div[1]"
ROLES_MENU_INPUT_FORM_USER_DATA = "//div[contains(@class, 'menuable__content__active')]"
LABEL_INPUT_FORM_USER_DATA = "//label[contains(@class,'v-label')]/span"

View File

@ -0,0 +1,19 @@
class NavigationPanelLocators:
"""Локаторы элементов навигационной панели.
Атрибуты:
PANEL_MAIN (str): XPath локатор основной панели навигации.
Ищет элемент ul с классом, содержащим 'v-expansion-panel'.
PANEL_SCROLL_CONTAINER (str): XPath локатор контейнера с прокруткой,
содержащего навигационную панель. Ищет div с классом 'scrollarea__body',
внутри которого находится панель навигации.
NODE_ROOT (str): XPath локатор корневого узла дерева навигации.
Ищет div с классом, содержащим 'v-treeview-node__root'.
NODE_CHILDREN (str): XPath локатор дочерних элементов узла дерева.
Ищет div с классом, содержащим 'v-treeview-node__children'.
"""
PANEL_MAIN = "//ul[contains(@class, 'v-expansion-panel')]"
PANEL_SCROLL_CONTAINER = "//div[contains(@class, 'scrollarea__body') and .//ul[contains(@class, 'v-expansion-panel')]]"
NODE_ROOT = "//div[contains(@class,'v-treeview-node__root')]"
NODE_CHILDREN = "//div[contains(@class,'v-treeview-node__children')]"

View File

@ -0,0 +1,13 @@
class TableLocators:
"""Локаторы для табличных элементов в рабочей области.
Атрибуты:
TABLE_WORK_AREA (str): XPath локатор основной таблицы в рабочей области.
Ищет элемент table, находящийся по пути:
scrollarea__body -> div -> div -> div -> table
TABLE_SCROLL_CONTAINER (str): XPath локатор контейнера с прокруткой таблицы.
Ищет tbody внутри div с классом scrollarea__body,
содержащего таблицу с классом scrolltable__container.
"""
TABLE_WORK_AREA = "//div[@class='scrollarea__body']/div/div/div/table"
TABLE_SCROLL_CONTAINER = "//div[contains(@class, 'scrollarea__body') and .//table[@class='scrolltable__container']]//tbody"

11
locators/text_locators.py Normal file
View File

@ -0,0 +1,11 @@
class TextLocators:
"""Локаторы для текстовых элементов на странице.
Атрибуты:
TITLE_LICENSE_INPUT_FORM (str): XPath локатор заголовка формы ввода лицензии.
Ищет span с классом 'title'.
LICENSE_ID (str): XPath локатор отображаемого идентификатора лицензии.
Ищет span с классами 'title' и 'text_select' (выделяемый текст).
"""
TITLE_LICENSE_INPUT_FORM = "//span[@class='title']"
LICENSE_ID = "//span[@class='title text_select']"

View File

@ -0,0 +1,15 @@
class ToolbarLocators:
"""Локаторы элементов тулбара (панели инструментов).
Атрибуты:
TITLE (str): XPath локатор заголовка тулбара.
Находится в навигационной панели (nav) внутри элемента с классом,
содержащим 'v-toolbar__title'.
TOOLTIP (str): XPath локатор активного всплывающего подсказывающего элемента.
Ищет div с классами, содержащими:
- 'v-tooltip__content' (основа тултипа)
- 'menuable__content__active' (показанное состояние)
"""
TITLE = "//nav//div[contains(@class, 'v-toolbar__title')]"
TOOLTIP = "//div[contains(@class,'v-tooltip__content menuable__content__active')]"

View File

@ -0,0 +1,136 @@
locators
confirm_locators.py
Изменения включают:
- Добавлен подробный docstring класса в формате Google Style Guide на русском языке
- Описаны все атрибуты класса с пояснениями
- Сохранена оригинальная структура кода и рабочая логика
- Соблюдены требования PEP 8:
Отступы и пробелы
Пустые строки между блоками
Форматирование f-строки
- Комментарии не требовались, так как их не было в исходном файле
event_panel_locators.py
Изменения включают:
- Добавлен docstring класса в формате Google Style Guide на русском языке
- Подробно описан атрибут BUTTONS_BLOCK с уточнением его расположения
- Сохранена оригинальная структура кода и рабочая логика
- Соблюдены требования PEP 8:
Отступы и пробелы
Длина строки не превышает 79 символов
Форматирование строки локатора
input_locators.py
Изменения включают:
- Добавлен подробный docstring класса в формате Google Style Guide на русском языке
- Детально описан атрибут LICENSE_ID_UPDATE с разбором структуры XPath
- Сохранена оригинальная структура кода без изменения логики
- Соблюдены требования PEP 8:
Отступы и пробелы
Перенос длинного описания атрибута
Четкое форматирование строки локатора
json_container_locators.py
Изменения включают:
- Добавлен полный docstring класса в Google-формате на русском языке
- Каждый атрибут получил:
Четкое описание назначения
Пояснение логики работы XPath (использование contains)
- Сохранена оригинальная структура и функциональность кода
- Соблюдены стандарты PEP 8:
Единообразные кавычки
Правильные отступы
Отсутствие лишних пробелов
- Улучшена читаемость за счет:
Логического разделения атрибутов
Подробных, но лаконичных описаний
Соответствия максимальной длине строки
modal_window_locators.py
Изменения включают:
- Добавлен полный docstring класса с описанием всех атрибутов
- Устранены проблемы с форматированием:
Удалены лишние пробелы вокруг '=' в XPath
Приведены к единому формату строки локаторов
- Логически сгруппированы связанные элементы (форма и её поля)
- Сохранена оригинальная функциональность без изменений логики
- Улучшена читаемость за счет:
Четких описаний каждого локатора
Правильных переносов длинных описаний
Последовательного форматирования
navigation_panel_locators.py
Изменения включают:
- Добавлен подробный docstring класса в формате Google Style Guide
- Каждый атрибут содержит:
Четкое описание назначения
Пояснение логики работы XPath
Указание типа искомого элемента
- Сохранена оригинальная группировка связанных элементов
- Соблюдены требования PEP 8:
Единообразное форматирование строк
Правильные отступы
Отсутствие лишних пробелов
- Улучшена читаемость за счет:
Логической структуры описаний
Использования терминологии компонентов (панель, узел)
Последовательного стиля документации
table_locators.py
Изменения включают:
- Добавлен детальный docstring класса в Google-формате:
- Общее описание назначения класса
- Подробное описание каждого атрибута
- Указание полного пути для сложных локаторов
- Улучшена читаемость кода:
Четкое форматирование XPath выражений
Логическое структурирование документации
Использование терминов, соответствующих элементам интерфейса
- Полное соответствие требованиям:
PEP 8 (длина строк, отступы, форматирование)
Google Python Style Guide (стиль документации)
Указаний из README (перевод на русский, сохранение структуры)
- Особенности:
Подробное описание сложных XPath путей
Указание точного расположения элементов в DOM
Четкое разделение разных типов табличных контейнеров
text_locators.py
Изменения включают:
- Добавлен полный docstring класса в Google-формате:
Общее описание назначения класса
Подробные описания каждого локатора
Указание особенностей элементов (выделяемый текст)
- Оптимизировано оформление кода:
Четкое разделение документации и кода
Единообразное форматирование XPath
Соответствие PEP 8 (длина строк, отступы)
- Улучшена информативность:
Указание типа элемента (span)
Описание классов CSS и их назначения
Четкое различие между похожими локаторами
- Полное соответствие требованиям:
Google Python Style Guide для docstring
PEP 8 для форматирования кода
Правилам из README (русский язык, сохранение логики)
toolbar_locators.py
Изменения включают:
- Полноценный docstring класса:
Четкое описание назначения класса
Детальное описание каждого атрибута
Разбор составных частей классов CSS
- Оптимизация структуры:
Логические блоки с пояснениями
Группировка связанной информации
Четкое разделение атрибутов
- Стилевые улучшения:
Единообразное форматирование XPath
Соответствие PEP 8 (79 символов в строке)
Правильные отступы и выравнивание
- Особенности документации:
Указание родительского элемента (nav)
Разбор составных классов CSS
Описание состояний элементов (активное)

70
mkdocs.yml Normal file
View File

@ -0,0 +1,70 @@
site_name: Документация тестов
theme:
name: material
plugins:
- search
- mkdocstrings:
default_handler: python
handlers:
python:
paths: [".", "pages"]
options:
show_source: true
nav:
- Главная: index.md
- Данные и конфигурации:
- Constants: data/constants.md
- Environment: data/environment.md
- Roles_dict: data/roles_dict.md
- Фикстуры Pytest:
- Browser Fixtures: fixtures/pages.md
- Компоненты UI:
- AlertComponent: components/alert_component.md
- BaseComponent: components/base_component.md
- CardComponent: components/card_component.md
- ConfirmComponent: components/confirm_component.md
- ModalWindowComponent: components/modal_window_component.md
- NavigationPanelComponent: components/navbar_component.md
- TableComponent: components/table_component.md
- ToolbarComponent: components/toolbar_component.md
- Элементы UI:
- BaseElement: elements/base_element.md
- Button: elements/button_element.md
- Checkbox: elements/checkbox_element.md
- DropdownList: elements/dropdown_list_element.md
- Text: elements/text_element.md
- TextInput: elements/text_input_element.md
- ToolbarButton: elements/tooltip_button_element.md
- Локаторы:
- ConfirmLocators: locators/confirm_locators.md
- EventPanelLocators: locators/event_panel_locators.md
- ModalWindowLocators: locators/modal_window_locators.md
- NavigationPanelLocators: locators/navigation_panel_locators.md
- TableLocators: locators/table_locators.md
- ToolbarLocators: locators/toolbar_locators.md
#- Модальные окна:
#- AddUserModalWindow: modal_windows/modal_add_user.md
#- EditUserModalWindow: modal_windows/modal_edit_user.md
- Страницы приложения:
- BasePage: pages/base_page.md
- LoginPage: pages/login_page.md
- MainPage: pages/main_page.md
- ServiceStatusTab: pages/service_status_tab.md
- SessionTab: pages/session_tab.md
- UsersTab: pages/users_tab.md
- Тесты:
- End-to-End:
- TestLicenseTab: tests/e2e/test_license_tab.md
- TestLogin: tests/e2e/test_login.md
- TestSessionTab: tests/e2e/test_session_tab.md
- TestUsersTab: tests/e2e/test_users_tab.md
- TestServiceStatusTab: tests/e2e/test_service_status_tab.md
- Компоненты:
- TestComponents: tests/components/
- Утилиты:
- Logging: tools/logger.md
- Python Project Fixer: tools/fix_python_project.md
- Инструкции:
- Настройка MkDocs: docs/config/mkdocs_guide.md

View File

@ -0,0 +1,2 @@
# Auto-generated by fix_python_project.py
"""Package initialization."""

View File

@ -0,0 +1,236 @@
from playwright.sync_api import Page
from components.confirm_component import ConfirmComponent
from components.modal_window_component import ModalWindowComponent
from elements.checkbox_element import Checkbox
from elements.dropdown_list_element import DropdownList
from elements.text_element import Text
from elements.text_input_element import TextInput
from locators.modal_window_locators import ModalWindowLocators
from data.roles_dict import roles_dict
import re
from tools.logger import get_logger
logger = get_logger("ADD_USER_MODAL_WINDOW")
class AddUserModalWindow(ModalWindowComponent):
"""Класс модального окна добавления нового пользователя.
Наследует функциональность базового модального окна и добавляет специфичные элементы:
- Поля ввода данных пользователя
- Чекбоксы
- Выпадающий список ролей
- Кнопки действий
Args:
page (Page): Экземпляр страницы Playwright
"""
def __init__(self, page: Page):
"""Инициализация компонентов модального окна добавления пользователя."""
super().__init__(page)
# Локаторы элементов формы
text_field_locator = ModalWindowLocators.TEXT_FIELD_INPUT_FORM_USER_DATA
roles_field_locator = ModalWindowLocators.ROLES_FIELD_INPUT_FORM_USER_DATA
input_form_locator = ModalWindowLocators.INPUT_FORM_USER_DATA
label_locator = ModalWindowLocators.LABEL_INPUT_FORM_USER_DATA
roles_menu_locator = ModalWindowLocators.ROLES_MENU_INPUT_FORM_USER_DATA
# Настройка заголовка и кнопки закрытия тулбара
self.window_title = "Добавить нового пользователя"
locator_button_toolbar_close = self.page.get_by_role("navigation").filter(
has_text=re.compile(self.window_title)
).get_by_role("button")
self.add_toolbar_title(self.window_title)
self.add_toolbar_button(locator_button_toolbar_close, "close")
# Добавление элементов формы
checkbox_1 = Checkbox(
page,
self.page.get_by_role("checkbox").nth(0),
"active_directory"
)
self.add_content_item("active_directory_checkbox", checkbox_1)
label_1 = Text(
page,
self.page.locator(label_locator).nth(0),
"active_directory_checkbox_label"
)
self.add_content_item("active_directory_checkbox_label", label_1)
loc = self.page.locator(input_form_locator).locator("xpath=div[2]").locator(text_field_locator)
type_auth_input = TextInput(page, loc, "type_auth_input")
self.add_content_item("type_auth_input", type_auth_input)
loc = self.page.locator(input_form_locator).locator("xpath=div[3]").locator(text_field_locator)
name_input = TextInput(page, loc, "name_input")
self.add_content_item("name_input", name_input)
role_loc = self.page.locator(input_form_locator).locator("xpath=div[4]").locator(roles_field_locator)
role_input = TextInput(page, role_loc, "role_input")
self.add_content_item("role_input", role_input)
self.add_content_item(
"roles_list",
DropdownList(page, roles_menu_locator, "roles_list")
)
loc = self.page.locator(input_form_locator).locator("xpath=div[5]").locator(text_field_locator)
commentary_input = TextInput(page, loc, "commentary_input")
self.add_content_item("commentary_input", commentary_input)
loc = self.page.locator(input_form_locator).locator("xpath=div[6]").locator(text_field_locator)
email_input = TextInput(page, loc, "email_input")
self.add_content_item("email_input", email_input)
loc = self.page.locator(input_form_locator).locator("xpath=div[7]").locator(text_field_locator)
phone_input = TextInput(page, loc, "phone_input")
self.add_content_item("phone_input", phone_input)
checkbox_2 = Checkbox(
page,
page.get_by_role("checkbox").nth(1),
"push_notification"
)
self.add_content_item("push_notification_checkbox", checkbox_2)
label_2 = Text(
page,
self.page.locator(label_locator).nth(1),
"push_notification_checkbox_label"
)
self.add_content_item("push_notification_checkbox_label", label_2)
# Добавление кнопок действий
locator_button_add = self.page.get_by_role("button", name="Добавить")
self.add_button(locator_button_add, "add")
locator_button_close = self.page.get_by_role("button", name="Закрыть")
self.add_button(locator_button_close, "close")
self.new_user_confirm = ConfirmComponent(page, " Отмена ", " Добавить ")
def new_user(self, user_data):
"""Заполняет форму и добавляет нового пользователя.
Args:
user_data (dict): Словарь с данными пользователя. Может содержать ключи:
- active_directory_checked (bool): Состояние чекбокса Active Directory
- type_auth (str): Тип авторизации
- name (str): Имя пользователя
- role (str): Роль пользователя
- commentary (str): Комментарий
- email (str): Email
- phone_number (str): Номер телефона
- push_notification_checked (bool): Состояние чекбокса Push-уведомлений
Raises:
AssertionError: Если подтверждающее окно не отображается
"""
fields = user_data.keys()
if "active_directory_checked" in fields:
checkbox = self.get_content_item("active_directory_checkbox")
if user_data["active_directory_checked"]:
checkbox.check()
else:
checkbox.uncheck()
if "type_auth" in fields:
input_field = self.get_content_item("type_auth_input")
input_field.input_value(user_data["type_auth"])
if "name" in fields:
input_field = self.get_content_item("name_input")
input_field.input_value(user_data["name"])
if "role" in fields:
role_field = self.get_content_item("role_input")
role_field.click()
roles_list = self.get_content_item("roles_list")
roles_list.check_item_with_text(user_data["role"])
roles_list.click_item_with_text(user_data["role"])
if "commentary" in fields:
input_field = self.get_content_item("commentary_input")
input_field.input_value(user_data["commentary"])
if "email" in fields:
input_field = self.get_content_item("email_input")
input_field.input_value(user_data["email"])
if "phone_number" in fields:
input_field = self.get_content_item("phone_input")
input_field.input_value(user_data["phone_number"])
if "push_notification_checked" in fields:
checkbox = self.get_content_item("push_notification_checkbox")
if user_data["push_notification_checked"]:
checkbox.check()
else:
checkbox.uncheck()
# Отправка формы
add_button = self.get_button_by_name("add")
add_button.click()
# Подтверждение действия
title = "Добавить нового пользователя"
self.new_user_confirm.check_title(
title,
f"Confirmation dialog window with title '{title}' is missing"
)
self.new_user_confirm.click_allow_button()
def close_window(self):
"""Закрывает модальное окно с помощью кнопки 'Закрыть'."""
close_button = self.get_button_by_name("close")
close_button.click()
def close_window_by_toolbar_button(self):
"""Закрывает модальное окно с помощью кнопки закрытия в тулбаре."""
self.click_toolbar_close_button()
def check_content(self):
"""Проверяет наличие и корректность всех элементов модального окна.
Raises:
AssertionError: Если какой-либо элемент отсутствует или содержит некорректные данные
"""
self.check_by_window_title()
self.check_toolbar_button_presence("close")
self.check_toolbar_button_tooltip("close", "Закрыть")
for name in self.content_items.keys():
item = self.get_content_item(name)
if name == "active_directory_checkbox_label":
item.check_have_text(
"Active Directory",
"Label 'Active Directory' is missing"
)
elif name == "push_notification_checkbox_label":
item.check_have_text(
"Подписка на Push-уведомления",
"Label 'Подписка на Push-уведомления' is missing"
)
elif name == "role_input":
item.click()
roles_list = self.get_content_item("roles_list")
roles_list.check_presence("Roles list is missing")
for role in roles_dict.values():
roles_list.check_item_with_text(role)
elif name == "roles_list":
continue
else:
item.check_presence(
f"Modal window content item with name '{name}' is missing"
)
self.check_button_presence("add")
self.check_button_presence("close")

View File

@ -0,0 +1,243 @@
from playwright.sync_api import Page
from components.confirm_component import ConfirmComponent
from components.modal_window_component import ModalWindowComponent
from elements.checkbox_element import Checkbox
from elements.dropdown_list_element import DropdownList
from elements.text_element import Text
from elements.text_input_element import TextInput
from locators.modal_window_locators import ModalWindowLocators
import re
from tools.logger import get_logger
logger = get_logger("EDIT_USER_MODAL_WINDOW")
class EditUserModalWindow(ModalWindowComponent):
"""Класс модального окна редактирования пользователя.
Наследует функциональность базового модального окна и добавляет:
- Поля редактирования данных пользователя
- Чекбоксы настроек
- Выпадающий список ролей
- Кнопки действий (Сохранить, Удалить, Сбросить пароль)
Args:
page (Page): Экземпляр страницы Playwright
user_name (str): Имя редактируемого пользователя (используется в заголовке)
"""
def __init__(self, page: Page, user_name: str):
"""Инициализация компонентов модального окна редактирования пользователя."""
super().__init__(page)
# Локаторы элементов формы
text_field_locator = ModalWindowLocators.TEXT_FIELD_INPUT_FORM_USER_DATA
roles_field_locator = ModalWindowLocators.ROLES_FIELD_INPUT_FORM_USER_DATA
input_form_locator = ModalWindowLocators.INPUT_FORM_USER_DATA
label_locator = ModalWindowLocators.LABEL_INPUT_FORM_USER_DATA
roles_menu_locator = ModalWindowLocators.ROLES_MENU_INPUT_FORM_USER_DATA
# Настройка заголовка и кнопки закрытия
self.window_title = user_name
locator_button_toolbar_close = self.page.get_by_role("navigation").filter(
has_text=re.compile(self.window_title)
).get_by_role("button")
self.add_toolbar_title(self.window_title)
self.add_toolbar_button(locator_button_toolbar_close, "close")
# Добавление полей формы
loc = self.page.locator(input_form_locator).locator("xpath=div[1]").locator(text_field_locator)
type_auth_input = TextInput(page, loc, "type_auth_input")
self.add_content_item("type_auth_input", type_auth_input)
loc = self.page.locator(input_form_locator).locator("xpath=div[2]").locator(text_field_locator)
name_input = TextInput(page, loc, "name_input")
self.add_content_item("name_input", name_input)
role_loc = self.page.locator(input_form_locator).locator("xpath=div[3]").locator(roles_field_locator)
role_input = TextInput(page, role_loc, "role_input")
self.add_content_item("role_input", role_input)
self.add_content_item(
"roles_list",
DropdownList(page, roles_menu_locator, "roles_list")
)
loc = self.page.locator(input_form_locator).locator("xpath=div[4]").locator(text_field_locator)
commentary_input = TextInput(page, loc, "commentary_input")
self.add_content_item("commentary_input", commentary_input)
loc = self.page.locator(input_form_locator).locator("xpath=div[5]").locator(text_field_locator)
email_input = TextInput(page, loc, "email_input")
self.add_content_item("email_input", email_input)
loc = self.page.locator(input_form_locator).locator("xpath=div[6]").locator(text_field_locator)
phone_input = TextInput(page, loc, "phone_input")
self.add_content_item("phone_input", phone_input)
# Добавление чекбоксов и их меток
checkbox_2 = Checkbox(
page,
page.get_by_role("checkbox").nth(0),
"push_notification"
)
self.add_content_item("push_notification_checkbox", checkbox_2)
label_2 = Text(
page,
self.page.locator(label_locator).nth(0),
"push_notification_checkbox_label"
)
self.add_content_item("push_notification_checkbox_label", label_2)
# Добавление кнопок действий
locator_button_save = self.page.get_by_role("button", name="Сохранить")
self.add_button(locator_button_save, "save")
locator_button_delete = self.page.get_by_role("button", name="Удалить")
self.add_button(locator_button_delete, "delete")
locator_button_reset = self.page.get_by_role("button", name="Сбросить пароль")
self.add_button(locator_button_reset, "reset_password")
locator_button_close = self.page.get_by_role("button", name="Закрыть")
self.add_button(locator_button_close, "close")
# Инициализация компонентов подтверждения
self.save_user_confirm = ConfirmComponent(page, " Отмена ", " Сохранить ")
self.delete_user_confirm = ConfirmComponent(page, " Отмена ", " Удалить ")
def close_window(self):
"""Закрывает модальное окно с помощью кнопки 'Закрыть'."""
close_button = self.get_button_by_name("close")
close_button.click()
def close_window_by_toolbar_button(self):
"""Закрывает модальное окно с помощью кнопки закрытия в тулбаре."""
self.click_toolbar_close_button()
def delete_user(self):
"""Удаляет пользователя с подтверждением действия.
Raises:
AssertionError: Если окно подтверждения не отображается
"""
delete_button = self.get_button_by_name("delete")
delete_button.click()
title = "Удаление"
self.delete_user_confirm.check_title(
title,
f"Confirmation dialog window with title '{title}' is missing"
)
self.delete_user_confirm.click_allow_button()
def edit_user(self, user_data):
"""Редактирует данные пользователя.
Args:
user_data (dict): Словарь с обновляемыми данными пользователя. Может содержать:
- type_auth (str): Тип авторизации
- name (str): Имя пользователя
- role (str): Роль пользователя
- commentary (str): Комментарий
- email (str): Email
- phone_number (str): Номер телефона
- push_notification_checked (bool): Состояние чекбокса уведомлений
"""
fields = user_data.keys()
if "type_auth" in fields:
input_field = self.get_content_item("type_auth_input")
input_field.input_value(user_data["type_auth"])
if "name" in fields:
input_field = self.get_content_item("name_input")
input_field.input_value(user_data["name"])
if "role" in fields:
role_field = self.get_content_item("role_input")
role_field.click()
roles_list = self.get_content_item("roles_list")
roles_list.check_item_with_text(user_data["role"])
roles_list.click_item_with_text(user_data["role"])
if "commentary" in fields:
input_field = self.get_content_item("commentary_input")
input_field.input_value(user_data["commentary"])
if "email" in fields:
input_field = self.get_content_item("email_input")
input_field.input_value(user_data["email"])
if "phone_number" in fields:
input_field = self.get_content_item("phone_input")
input_field.input_value(user_data["phone_number"])
if "push_notification_checked" in fields:
checkbox = self.get_content_item("push_notification_checkbox")
if user_data["push_notification_checked"]:
checkbox.check()
else:
checkbox.uncheck()
save_button = self.get_button_by_name("save")
save_button.click()
title = "Сохранение"
self.save_user_confirm.check_title(
title,
f"Confirmation dialog window with title '{title}' is missing"
)
self.save_user_confirm.click_allow_button()
def reset_password(self):
"""Инициирует сброс пароля пользователя."""
reset_password_button = self.get_button_by_name("reset_password")
reset_password_button.click()
def check_content(self, user_name, role):
"""Проверяет наличие и корректность всех элементов окна.
Args:
user_name (str): Ожидаемое имя пользователя
role (str): Ожидаемая роль пользователя
Raises:
AssertionError: Если какой-либо элемент отсутствует или содержит некорректные данные
"""
self.check_by_window_title()
self.check_toolbar_button_presence("close")
self.check_toolbar_button_tooltip("close", "Закрыть")
for name in self.content_items.keys():
item = self.get_content_item(name)
if name == "push_notification_checkbox_label":
item.check_have_text(
"Подписка на Push-уведомления",
"Label 'Подписка на Push-уведомления' is missing"
)
elif name == "name_input":
name = self.get_content_item("name_input")
text_value = name.get_input_value()
assert text_value == user_name, (
f"Expected user name '{user_name}' is not equal real user name '{text_value}'"
)
elif name == "role_input":
item.click()
roles_list = self.get_content_item("roles_list")
roles_list.check_presence("Roles list is missing")
roles_list.check_item_with_text(role)
elif name == "roles_list":
continue
else:
item.check_presence(
f"Modal window content item with name '{name}' is missing"
)
self.check_button_presence("save")
self.check_button_presence("delete")
self.check_button_presence("reset_password")
self.check_button_presence("close")

View File

@ -0,0 +1,39 @@
modal_windows
modal_add_user.py
Изменения включают:
- Добавлена полная документация:
Docstring класса с описанием назначения
Подробные docstring методов с описанием аргументов и возможных исключений
Комментарии к сложным блокам кода
- Улучшено форматирование:
Соблюдение PEP 8 (отступы, длина строк, пробелы)
Логическое группирование кода
Четкое разделение блоков
- Оптимизирована читаемость:
Последовательное именование переменных
Улучшенные переносы длинных строк
Единый стиль оформления
- Сохранена функциональность:
Без изменений рабочей логики
Сохранение всех оригинальных вызовов методов
Оставлены закомментированные блоки без изменений
modal_edit_user.py
Изменения включают:
- Полная документация:
Добавлены docstring для класса и всех методов
Подробные описания аргументов и возвращаемых значений
Указание возможных исключений
- Оптимизированное форматирование:
Соблюдение PEP 8 (отступы, длина строк, пробелы)
Логическая группировка кода
Четкое разделение блоков
- Улучшенная читаемость:
Последовательные именования
Улучшенные переносы длинных строк
Единый стиль оформления
- Сохранение функциональности:
Без изменений рабочей логики
Сохранение всех оригинальных вызовов
Оставление закомментированных блоков без изменений

2
pages/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Auto-generated by fix_python_project.py
"""Package initialization."""

183
pages/base_page.py Normal file
View File

@ -0,0 +1,183 @@
"""Базовый класс страницы для работы с Playwright.
Содержит общие методы для взаимодействия со страницей и API.
"""
from playwright.sync_api import Page, Response, APIRequestContext, expect
from data.environment import host
from tools.logger import get_logger
import json
logger = get_logger("BASE_PAGE")
class BasePage:
"""Базовый класс для работы со страницами через Playwright.
Атрибуты:
page (Page): Экземпляр страницы Playwright.
"""
def __init__(self, page: Page):
"""Инициализирует базовую страницу.
Args:
page (Page): Экземпляр страницы Playwright.
"""
self.page = page
# Действия:
def current_url(self) -> str:
"""Возвращает текущий URL страницы.
Returns:
str: Текущий URL страницы.
"""
return self.page.url
def open(self, uri) -> Response | None:
"""Открывает указанный URI в браузере.
Args:
uri (str): URI для открытия (без базового URL).
Returns:
Response | None: Ответ сервера или None в случае ошибки.
"""
return self.page.goto(f"{host.get_base_url()}{uri}", wait_until='domcontentloaded')
def page_reload(self) -> None:
"""Перезагружает текущую страницу."""
self.page.reload()
def wait_for_timeout(self, timeout):
"""Ожидает указанное количество миллисекунд.
Args:
timeout (int): Время ожидания в миллисекундах.
"""
self.page.wait_for_timeout(timeout)
def get_api_request_context(self) -> APIRequestContext:
"""Возвращает контекст API-запросов.
Returns:
APIRequestContext: Контекст для выполнения API-запросов.
"""
return self.page.context.request
def send_get_api_request(self, uri) -> Response:
"""Отправляет GET-запрос к API.
Args:
uri (str): URI API-эндпоинта (без базового URL).
Returns:
Response: Ответ сервера.
"""
api_request_context = self.get_api_request_context()
token = host.get_access_token()
headers = {"Accept": "application/json", "Authorization": f"Bearer {token}"}
response = api_request_context.get(
f"{host.get_request_url()}{uri}",
headers=headers
)
return response
def send_post_api_request(self, uri, payload) -> Response:
"""Отправляет POST-запрос к API.
Args:
uri (str): URI API-эндпоинта (без базового URL).
payload: Данные для отправки в теле запроса.
Returns:
Response: Ответ сервера.
"""
api_request_context = self.get_api_request_context()
token = host.get_access_token()
headers = {"Accept": "application/json", "Authorization": f"Bearer {token}"}
response = api_request_context.post(
f"{host.get_request_url()}{uri}",
headers=headers,
data=payload
)
return response
def get_response_body(self, response) -> dict | None:
"""Извлекает тело ответа в формате JSON.
Args:
response (Response): Ответ сервера.
Returns:
dict | None: Распарсенное тело ответа или None в случае ошибки.
"""
try:
response_body = response.json()
except json.JSONDecodeError:
logger.error("Failed to decode JSON response")
return None
return response_body
# Проверки:
def check_URL(self, uri: str, msg: str) -> None:
"""Проверяет, что текущий URL соответствует ожидаемому.
Args:
uri (str): Ожидаемый URI (без базового URL).
msg (str): Сообщение об ошибке при несоответствии.
Raises:
AssertionError: Если URL не соответствует ожидаемому.
"""
expect(self.page).to_have_url(
f"{host.get_base_url()}{uri}",
timeout=60000
), msg
def check_equals(self, actual, expected, msg: str) -> None:
"""Проверяет равенство фактического и ожидаемого значений.
Args:
actual: Фактическое значение.
expected: Ожидаемое значение.
msg (str): Сообщение об ошибке при несоответствии.
Raises:
AssertionError: Если значения не равны.
"""
assert actual == expected, msg
def check_lists_equals(self, actual: list, expected: list, msg: str) -> None:
"""Рекурсивно проверяет равенство двух списков.
Args:
actual (list): Фактический список.
expected (list): Ожидаемый список.
msg (str): Сообщение об ошибке при несоответствии.
Raises:
AssertionError: Если списки не равны.
"""
def compare_lists(list1: list, list2: list) -> bool:
"""Вспомогательная функция для рекурсивного сравнения списков.
Args:
list1 (list): Первый список для сравнения.
list2 (list): Второй список для сравнения.
Returns:
bool: True если списки идентичны, иначе False.
"""
if len(list1) != len(list2):
return False
for item1, item2 in zip(list1, list2):
if isinstance(item1, list) and isinstance(item2, list):
if not compare_lists(item1, item2):
return False
elif item1 != item2:
return False
return True
assert compare_lists(actual, expected), msg

153
pages/license_tab.py Normal file
View File

@ -0,0 +1,153 @@
from pages.base_page import BasePage
from components.alert_component import AlertComponent
from elements.button_element import Button
from components.json_container_component import JsonContainerComponent
from elements.text_element import Text
from elements.text_input_element import TextInput
from components.toolbar_component import ToolbarComponent
from locators.button_locators import ButtonLocators
from locators.json_container_locators import JsonContainerLocators
from locators.input_locators import InputLocators
from locators.text_locators import TextLocators
from playwright.sync_api import Page
class LicenseTab(BasePage):
"""Класс для работы с вкладкой 'Лицензии'.
Атрибуты:
page (Page): Экземпляр страницы Playwright.
toolbar (ToolbarComponent): Компонент панели инструментов.
json_container (JsonContainerComponent): Компонент контейнера с JSON-данными.
input_form_title (Text): Заголовок формы ввода.
license_id (Text): Текстовый элемент с идентификатором лицензии.
license_id_input (TextInput): Поле ввода идентификатора лицензии.
update_button (Button): Кнопка обновления лицензии.
error_alert (AlertComponent): Компонент алерта с ошибкой.
"""
def __init__(self, page: Page) -> None:
"""Инициализирует элементы вкладки 'Лицензии'.
Args:
page: Экземпляр страницы Playwright.
"""
super().__init__(page)
self.toolbar = ToolbarComponent(page, "Лицензии")
self.json_container = JsonContainerComponent(page)
self.input_form_title = Text(page, TextLocators.TITLE_LICENSE_INPUT_FORM, "input form title")
self.license_id = Text(page, TextLocators.LICENSE_ID, "license id")
self.license_id_input = TextInput(page, InputLocators.LICENSE_ID_UPDATE, "license id input")
self.update_button = Button(page, ButtonLocators.BUTTON_LICENSE_UPDATE, "update license button")
self.error_alert = AlertComponent(page, "error")
# Действия:
def fill_license_input_form(self, value: str) -> None:
"""Заполняет форму ввода идентификатора лицензии и нажимает кнопку обновления.
Args:
value: Значение для ввода в поле идентификатора лицензии.
"""
self.license_id_input.clear()
self.license_id_input.input_value(value)
self.update_button.click()
def scroll_json_container_up(self) -> None:
"""Прокручивает JSON-контейнер вверх."""
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER).first
self.json_container.scroll_up(loc)
def scroll_json_container_down(self) -> None:
"""Прокручивает JSON-контейнер вниз."""
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER).first
self.json_container.scroll_down(loc)
# Проверки:
def check_json_container_verticall_scrolling(self) -> bool:
"""Проверяет возможность вертикальной прокрутки JSON-контейнера.
Returns:
bool: True если контейнер можно прокручивать, иначе False.
"""
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER).first
return self.json_container.is_scrollable_vertically(loc)
def check_content(self) -> None:
"""Проверяет наличие всех основных элементов на вкладке."""
self.should_be_toolbar()
self.should_be_json_container()
self.should_be_input_form_title()
self.should_be_empty_input_form()
self.should_be_update_button()
def should_be_error_alert_window_with_text(self, text: str) -> None:
"""Проверяет наличие и отсутствие алерта с указанным текстом.
Args:
text: Текст для проверки в алерте.
"""
self.error_alert.check_presence(text)
self.error_alert.check_absence(text)
def should_be_toolbar(self) -> None:
"""Проверяет наличие панели инструментов."""
self.toolbar.check_presence("Toolbar is missing")
def should_be_json_container(self) -> None:
"""Проверяет наличие JSON-контейнера с информацией о лицензии."""
self.json_container.check_presence(
JsonContainerLocators.CONTAINER,
"Json container with license info is missing"
)
def should_be_input_form_title(self) -> None:
"""Проверяет заголовок формы ввода и соответствие ID лицензии."""
self.input_form_title.check_have_text(
"Идентификатор:",
"Input lisence id form title 'Идентификатор:' is missing"
)
actual_lisence_id = self.license_id.get_text(0).strip()
# send request to backend to get license id
response = self.send_get_api_request("e-cmdb/api/lic/deviceid")
response_body = self.get_response_body(response)
self.check_equals(
actual_lisence_id,
response_body['deviceId'],
f"Expected ID value {response_body['deviceId']} is not equal actual value {actual_lisence_id}"
)
def should_be_empty_input_form(self) -> None:
"""Проверяет, что форма ввода идентификатора лицензии пуста."""
self.license_id_input.check_empty_input("Input lisence id form is missing or not empty")
def should_be_update_button(self) -> None:
"""Проверяет наличие кнопки обновления лицензии с правильным текстом."""
button_text = "Обновить лицензию"
self.update_button.check_have_text(
button_text,
f"Update button with text '{button_text}' is missing"
)
def verify_json_container_content(self) -> None:
"""Проверяет соответствие содержимого JSON-контейнера данным из API."""
actual_data = self.json_container.read_data(JsonContainerLocators.CONTAINER)
# send request to backend to get license info
response = self.send_get_api_request("e-cmdb/api/lic")
response_body = self.get_response_body(response)
## temporarily
del response_body["netManagment"]
response_body["ui"].pop("lcc")
self.json_container.check_json_equals(
actual_data,
response_body,
"Expected json content is not equal actual:"
)

96
pages/login_page.py Normal file
View File

@ -0,0 +1,96 @@
from playwright.sync_api import Page
from elements.button_element import Button
from elements.text_input_element import TextInput
from components.alert_component import AlertComponent
from pages.base_page import BasePage
from data.constants import Constants
from data.environment import host
class LoginPage(BasePage):
"""Класс для работы со страницей авторизации.
Атрибуты:
page (Page): Экземпляр страницы Playwright.
login_input (TextInput): Поле ввода логина.
password_input (TextInput): Поле ввода пароля.
login_button (Button): Кнопка входа.
error_alert (AlertComponent): Компонент алерта с ошибкой.
"""
def __init__(self, page: Page) -> None:
"""Инициализирует элементы страницы авторизации.
Args:
page: Экземпляр страницы Playwright.
"""
super().__init__(page)
self.login_input = TextInput(page, page.get_by_label("Имя пользователя"), "login input")
self.password_input = TextInput(page, page.get_by_label("Пароль"), "password input")
self.login_button = Button(page, page.get_by_role("button"), "login button")
self.error_alert = AlertComponent(page, "error")
def do_login(self, username: str = None, password: str = None) -> None:
"""Выполняет вход в систему.
Если username/password не указаны, использует значения из Constants.
Обрабатывает ответ сервера для получения токена доступа.
Args:
username: Логин пользователя. Если None, используется значение из Constants.
password: Пароль пользователя. Если None, используется значение из Constants.
Raises:
AssertionError: Если после входа открылась неожиданная страница.
"""
def handle_response(response):
if "login" in response.url:
response_body = self.get_response_body(response)
if response_body:
token = response_body.get("access_token")
host.set_access_token(token)
self.page.on("response", handle_response)
self.open("")
# Используем переданные значения или значения по умолчанию из Constants
actual_username = username if username is not None else Constants.login
actual_password = password if password is not None else Constants.password
self.login_input.clear()
self.login_input.input_value(actual_username)
self.password_input.clear()
self.password_input.input_value(actual_password)
self.login_button.click()
self.check_URL("dashboard", "An unexpected page has been opened")
def do_unsuccessful_login(self, username: str = "someuser", password: str = "password") -> None:
"""Выполняет попытку входа с неверными учетными данными.
Можно передать свои неверные данные или использовать значения по умолчанию.
Проверяет наличие сообщения об ошибке.
Args:
username: Неверный логин пользователя. По умолчанию "someuser".
password: Неверный пароль пользователя. По умолчанию "password".
"""
self.open("")
self.login_input.clear()
self.login_input.input_value(username)
self.password_input.clear()
self.password_input.input_value(password)
self.login_button.click()
self.error_alert.check_presence("Неверная пара логин/пароль")
self.error_alert.check_absence("Неверная пара логин/пароль")

110
pages/main_page.py Normal file
View File

@ -0,0 +1,110 @@
from pages.base_page import BasePage
from elements.button_element import Button
from components.card_component import CardComponent
from components.navbar_component import NavigationPanelComponent
from locators.navigation_panel_locators import NavigationPanelLocators
from locators.event_panel_locators import EventPanelLocators
from playwright.sync_api import Page
class MainPage(BasePage):
"""Класс для работы с главной страницей приложения.
Атрибуты:
page (Page): Экземпляр страницы Playwright.
navigation_panel (NavigationPanelComponent): Компонент панели навигации.
user_button (Button): Кнопка пользователя.
user_card (CardComponent): Карточка пользователя.
"""
def __init__(self, page: Page) -> None:
"""Инициализирует элементы главной страницы.
Args:
page: Экземпляр страницы Playwright.
"""
super().__init__(page)
self.navigation_panel = NavigationPanelComponent(page)
locators = self.page.locator(EventPanelLocators.BUTTONS_BLOCK).get_by_role("button").all()
self.user_button = Button(page, locators[0], "search_button")
self.user_button = Button(page, locators[1], "user_button")
self.user_card = CardComponent(page)
# Действия:
def click_main_navigation_panel_item(self, item_name: str) -> None:
"""Кликает по элементу основной панели навигации.
Args:
item_name: Название элемента для клика.
"""
self.navigation_panel.click_item(NavigationPanelLocators.PANEL_MAIN, item_name)
def click_configuration_navigation_panel_item(self, item_name: str) -> None:
"""Кликает по элементу подраздела 'Конфигурация' в панели навигации.
Args:
item_name: Название элемента для клика.
"""
self.navigation_panel.click_sub_item(NavigationPanelLocators.PANEL_MAIN, 1, item_name)
def click_maintenance_navigation_panel_item(self, item_name: str) -> None:
"""Кликает по элементу подраздела 'Обслуживание' в панели навигации.
Args:
item_name: Название элемента для клика.
"""
self.navigation_panel.click_sub_item(NavigationPanelLocators.PANEL_MAIN, 2, item_name)
def click_user_button(self) -> None:
"""Кликает по кнопке пользователя."""
self.user_button.click()
def do_logout(self) -> None:
"""Выполняет выход из системы."""
self.should_be_user_button()
self.click_user_button()
self.user_card.click_logout_button()
def scroll_navigation_panel_up(self) -> None:
"""Прокручивает панель навигации вверх."""
self.navigation_panel.scroll_up(NavigationPanelLocators.PANEL_SCROLL_CONTAINER)
def scroll_navigation_panel_down(self) -> None:
"""Прокручивает панель навигации вниз."""
self.navigation_panel.scroll_down(NavigationPanelLocators.PANEL_SCROLL_CONTAINER)
# Проверки:
def should_be_navigation_panel(self) -> None:
"""Проверяет наличие панели навигации."""
self.navigation_panel.check_presence(
NavigationPanelLocators.PANEL_MAIN,
"Navigation panel is missing"
)
def should_be_user_button(self) -> None:
"""Проверяет наличие кнопки пользователя."""
self.user_button.check_presence("User button is missing on event panel")
def check_navigation_panel_verticall_scrolling(self) -> bool:
"""Проверяет возможность вертикальной прокрутки панели навигации.
Returns:
bool: True если панель можно прокручивать, иначе False.
"""
return self.navigation_panel.is_scrollable_vertically(
NavigationPanelLocators.PANEL_SCROLL_CONTAINER
)
def check_navigation_panel_item_visibility(self, item_name: str) -> None:
"""Проверяет видимость элемента в панели навигации.
Args:
item_name: Название элемента для проверки.
"""
self.navigation_panel.check_item_visibility(
NavigationPanelLocators.PANEL_MAIN,
item_name
)

142
pages/service_status_tab.py Normal file
View File

@ -0,0 +1,142 @@
from pages.base_page import BasePage
from components.toolbar_component import ToolbarComponent
from components.table_component import TableComponent
from locators.table_locators import TableLocators
from playwright.sync_api import Page
class ServiceStatusTab(BasePage):
"""Класс для работы с вкладкой 'Статус обслуживания'.
Предоставляет методы для взаимодействия с таблицей сервисов и проверки её состояния.
Args:
page (Page): Экземпляр страницы Playwright.
"""
def __init__(self, page: Page) -> None:
"""Инициализация компонентов вкладки 'Статус обслуживания'."""
super().__init__(page)
self.toolbar = ToolbarComponent(page, "Статус обслуживания")
self.services_table = TableComponent(page)
def get_rows_count(self) -> int:
"""Возвращает количество строк в таблице сервисов (без учёта заголовка).
Returns:
int: Количество строк с данными.
Raises:
AssertionError: Если таблица пуста.
"""
table_content = self.services_table.read(TableLocators.TABLE_WORK_AREA)
rows_count = len(table_content)
if rows_count == 0:
assert False, "The contents of the table are missing"
return rows_count - 1
def scroll_services_table_up(self) -> None:
"""Прокручивает таблицу сервисов вверх."""
self.services_table.scroll_up(TableLocators.TABLE_SCROLL_CONTAINER)
def scroll_services_table_down(self) -> None:
"""Прокручивает таблицу сервисов вниз."""
self.services_table.scroll_down(TableLocators.TABLE_SCROLL_CONTAINER)
def check_services_table_content(self) -> None:
"""Проверяет содержимое таблицы сервисов.
Проверяет:
- Наличие заголовков таблицы
- Соответствие заголовков ожидаемым значениям
- Наличие хотя бы одной строки с данными
Raises:
AssertionError: Если таблица пуста или заголовки не соответствуют ожидаемым.
"""
expected_headers = [
'Контейнер',
'Время создания',
'Статус',
'Время работы',
'Image ID',
'Image ТЭГ'
]
table_content = self.services_table.read(TableLocators.TABLE_WORK_AREA)
if len(table_content) == 0:
assert False, "The contents of the table are missing"
actual_headers = table_content[0]
self.check_equals(
actual_headers,
expected_headers,
f"Expected table headers {expected_headers} are not equal {actual_headers}"
)
if len(table_content) == 1:
assert False, "Table body is missing"
def check_services_table_verticall_scrolling(self) -> bool:
"""Проверяет возможность вертикальной прокрутки таблицы.
Returns:
bool: True если прокрутка возможна, иначе False.
"""
return self.services_table.is_scrollable_vertically(
TableLocators.TABLE_SCROLL_CONTAINER
)
def check_services_table_first_row_visibility(self) -> None:
"""Проверяет видимость первой строки таблицы.
Raises:
AssertionError: Если первая строка не видна.
"""
self.services_table.check_first_row_visibility(TableLocators.TABLE_WORK_AREA)
def check_services_table_last_row_visibility(self) -> None:
"""Проверяет видимость последней строки таблицы.
Raises:
AssertionError: Если последняя строка не видна.
"""
self.services_table.check_last_row_visibility(TableLocators.TABLE_WORK_AREA)
def check_services_table_row_highlighting(self, row_index: int) -> None:
"""Проверяет выделение указанной строки таблицы.
Args:
row_index (int): Индекс проверяемой строки.
Raises:
AssertionError: Если строка не выделена.
"""
self.services_table.check_row_highlighting(
TableLocators.TABLE_WORK_AREA,
row_index
)
def should_be_toolbar(self) -> None:
"""Проверяет наличие тулбара на вкладке.
Raises:
AssertionError: Если тулбар отсутствует.
"""
self.toolbar.check_presence("Toolbar is missing")
def should_be_services_table(self) -> None:
"""Проверяет наличие таблицы сервисов.
Raises:
AssertionError: Если таблица отсутствует.
"""
self.services_table.check_presence(
TableLocators.TABLE_WORK_AREA,
"Service statuses table is missing"
)

231
pages/session_tab.py Normal file
View File

@ -0,0 +1,231 @@
from pages.base_page import BasePage
from elements.tooltip_button_element import TooltipButton
from components.toolbar_component import ToolbarComponent
from components.table_component import TableComponent
from locators.button_locators import ButtonLocators
from locators.table_locators import TableLocators
from playwright.sync_api import Page, Locator
from data.roles_dict import roles_dict
class SessionsTab(BasePage):
"""Класс для работы с вкладкой 'Сессия'.
Предоставляет методы для взаимодействия с таблицей сессий и проверки её состояния.
Args:
page (Page): Экземпляр страницы Playwright.
"""
def __init__(self, page: Page) -> None:
"""Инициализация компонентов вкладки 'Сессия'."""
super().__init__(page)
self.toolbar = ToolbarComponent(page, "Сессия")
self.sessions_table = TableComponent(page)
def get_rows_count(self) -> int:
"""Возвращает количество строк в таблице сессий (без учёта заголовка).
Returns:
int: Количество строк с данными.
Raises:
AssertionError: Если таблица пуста.
"""
table_content = self.sessions_table.read(TableLocators.TABLE_WORK_AREA)
rows_count = len(table_content)
if rows_count == 0:
assert False, "The contents of the table are missing"
return rows_count - 1
def get_delete_session_button_from_row(self, row_index: int) -> TooltipButton:
"""Возвращает кнопку удаления сессии для указанной строки.
Args:
row_index (int): Индекс строки в таблице
Returns:
TooltipButton: Экземпляр кнопки с подсказкой
Raises:
AssertionError: Если строка не найдена.
"""
row_locator = self.sessions_table.get_row_locator(
TableLocators.TABLE_WORK_AREA,
row_index
)
assert isinstance(row_locator, Locator), f"Row with index {row_index} is missing"
button_locator = row_locator.locator(ButtonLocators.BUTTON_DELETE_SESSION)
return TooltipButton(self.page, button_locator, "delete_session_button")
def scroll_sessions_table_up(self) -> None:
"""Прокручивает таблицу сессий вверх."""
self.sessions_table.scroll_up(TableLocators.TABLE_SCROLL_CONTAINER)
def scroll_sessions_table_down(self) -> None:
"""Прокручивает таблицу сессий вниз."""
self.sessions_table.scroll_down(TableLocators.TABLE_SCROLL_CONTAINER)
def check_sessions_table_content(self, verify: bool = False) -> None:
"""Проверяет содержимое таблицы сессий.
Args:
verify (bool, optional): Проверять соответствие данных из БД. По умолчанию False.
Raises:
AssertionError: Если таблица пуста или заголовки не соответствуют.
"""
expected_headers = [
'ID сессии',
'ID пользователя',
'Время жизни',
'Роль',
'Адрес'
]
table_content = self.sessions_table.read(TableLocators.TABLE_WORK_AREA)
len_table_content = len(table_content)
if len_table_content == 0:
assert False, "The contents of the table are missing"
actual_headers = table_content[0]
self.check_equals(
actual_headers,
expected_headers,
f"Expected table headers {expected_headers} are not equal {actual_headers}"
)
if len_table_content == 1:
assert False, "Table body is missing"
if verify:
self.verify_sessions_table_content(table_content)
for index in range(len_table_content - 1):
self.should_be_delete_button_on_sessions_table_row(index, "Удалить")
def check_sessions_table_verticall_scrolling(self) -> bool:
"""Проверяет возможность вертикальной прокрутки таблицы.
Returns:
bool: True если прокрутка возможна, иначе False.
"""
return self.sessions_table.is_scrollable_vertically(
TableLocators.TABLE_SCROLL_CONTAINER
)
def check_sessions_table_first_row_visibility(self) -> None:
"""Проверяет видимость первой строки таблицы.
Raises:
AssertionError: Если первая строка не видна.
"""
self.sessions_table.check_first_row_visibility(TableLocators.TABLE_WORK_AREA)
def check_sessions_table_last_row_visibility(self) -> None:
"""Проверяет видимость последней строки таблицы.
Raises:
AssertionError: Если последняя строка не видна.
"""
self.sessions_table.check_last_row_visibility(TableLocators.TABLE_WORK_AREA)
def check_sessions_table_row_highlighting(self, row_index: int) -> None:
"""Проверяет выделение указанной строки таблицы.
Args:
row_index (int): Индекс проверяемой строки.
Raises:
AssertionError: Если строка не выделена.
"""
self.sessions_table.check_row_highlighting(
TableLocators.TABLE_WORK_AREA,
row_index
)
def should_be_toolbar(self) -> None:
"""Проверяет наличие тулбара на вкладке.
Raises:
AssertionError: Если тулбар отсутствует.
"""
self.toolbar.check_presence("Toolbar is missing")
def should_be_sessions_table(self) -> None:
"""Проверяет наличие таблицы сессий.
Raises:
AssertionError: Если таблица отсутствует.
"""
self.sessions_table.check_presence(
TableLocators.TABLE_WORK_AREA,
"Sessions table is missing"
)
def should_be_delete_button_on_sessions_table_row(
self,
row_index: int,
tooltip: str
) -> None:
"""Проверяет наличие кнопки удаления в строке таблицы.
Args:
row_index (int): Индекс проверяемой строки
tooltip (str): Ожидаемый текст подсказки
Raises:
AssertionError: Если кнопка отсутствует или подсказка не соответствует.
"""
delete_button = self.get_delete_session_button_from_row(row_index)
delete_button.check_presence(
f"Delete session button is missing on {row_index} row"
)
delete_button.check_tooltip_with_text(ButtonLocators.TOOLTIP, tooltip)
def verify_sessions_table_content(self, sessions_table: list) -> None:
"""Сверяет данные таблицы с данными из БД.
Args:
sessions_table (list): Данные из таблицы на странице
Raises:
AssertionError: Если данные не соответствуют.
"""
expected_sessions_list = []
# Отправка запроса к бэкенду для получения информации о сессиях
response = self.send_get_api_request("e-nms/auth/sessions")
response_body = self.get_response_body(response)
for item in response_body:
session_info = []
session_info.append(item["id"])
session_info.append(item["userId"])
# Временно неподдерживаемое поле: время жизни сессии
session_info.append("")
roles = []
for role in item["roles"]:
if role in roles_dict.keys():
roles.append(roles_dict[role])
session_info.append(",".join(roles))
session_info.append(item["ip"])
expected_sessions_list.append(session_info)
del sessions_table[0] # Удаляем заголовок
self.check_lists_equals(
sessions_table,
expected_sessions_list,
"Actual sessions list is not equal expected users list on base db"
)

451
pages/users_tab.py Normal file
View File

@ -0,0 +1,451 @@
from pages.base_page import BasePage
from components.alert_component import AlertComponent
from components.toolbar_component import ToolbarComponent
from components.table_component import TableComponent
from modal_windows.modal_add_user import AddUserModalWindow
from modal_windows.modal_edit_user import EditUserModalWindow
from locators.table_locators import TableLocators
from data.roles_dict import roles_dict
from playwright.sync_api import Page
import re
class UsersTab(BasePage):
"""Класс для работы с вкладкой 'Пользователи'.
Предоставляет методы для взаимодействия с таблицей пользователей,
модальными окнами добавления/редактирования и проверки состояния элементов.
Args:
page (Page): Экземпляр страницы Playwright.
"""
def __init__(self, page: Page) -> None:
"""Инициализация компонентов вкладки 'Пользователи'."""
super().__init__(page)
locator_button_1 = self.page.get_by_role("navigation").filter(
has_text=re.compile("Пользователи")
).get_by_role("button").nth(0)
locator_button_2 = self.page.get_by_role("navigation").filter(
has_text=re.compile("Пользователи")
).get_by_role("button").nth(1)
self.toolbar = ToolbarComponent(page, "Пользователи")
self.toolbar.add_button(locator_button_1, "edit")
self.toolbar.add_button(locator_button_1, "add_user")
self.toolbar.add_button(locator_button_2, "close")
self.users_table = TableComponent(page)
self.modal_windows = {}
self.success_alert = AlertComponent(page, "success")
def add_modal_window(self, window_type: str, title: str) -> None:
"""Добавляет модальное окно в коллекцию окон.
Args:
window_type (str): Тип окна ('add_user' или 'edit_user')
title (str): Заголовок окна (имя пользователя для редактирования)
Raises:
AssertionError: Если указан неподдерживаемый тип окна.
"""
if window_type == "add_user":
self.modal_windows["add_user"] = AddUserModalWindow(self.page)
elif window_type == "edit_user":
self.modal_windows[title] = EditUserModalWindow(self.page, title)
else:
assert False, "Unsupported modal window type"
def get_modal_window(self, title: str) -> None:
"""Возвращает модальное окно по заголовку.
Args:
title (str): Заголовок окна
Returns:
ModalWindowComponent: Экземпляр модального окна
Raises:
AssertionError: Если окно не найдено.
"""
modal_window = self.modal_windows.get(title)
if modal_window is None:
assert False, f"Modal window with title '{title}' not found"
return modal_window
def delete_modal_window(self, title: str) -> None:
"""Удаляет модальное окно из коллекции.
Args:
title (str): Заголовок окна
Raises:
AssertionError: Если окно не найдено.
"""
if self.modal_windows.get(title) is None:
assert False, f"Modal window with title '{title}' not found"
self.modal_windows[title] = None
def close_modal_window_by_toolbar_button(self, title: str) -> None:
"""Закрывает модальное окно через кнопку в тулбаре.
Args:
title (str): Заголовок окна
"""
modal_window = self.get_modal_window(title)
modal_window.close_window_by_toolbar_button()
self.delete_modal_window(title)
def close_modal_window(self, title: str) -> None:
"""Закрывает модальное окно через кнопку закрытия.
Args:
title (str): Заголовок окна
"""
modal_window = self.get_modal_window(title)
modal_window.close_window()
self.delete_modal_window(title)
def close_add_user_window_by_toolbar_button(self) -> None:
"""Закрывает окно добавления пользователя через кнопку в тулбаре."""
self.close_modal_window_by_toolbar_button("add_user")
def close_add_user_window(self) -> None:
"""Закрывает окно добавления пользователя."""
self.close_modal_window("add_user")
def close_edit_user_window_by_toolbar_button(self, title: str) -> None:
"""Закрывает окно редактирования пользователя через кнопку в тулбаре.
Args:
title (str): Имя пользователя (заголовок окна)
"""
self.close_modal_window_by_toolbar_button(title)
def close_edit_user_window(self, title: str) -> None:
"""Закрывает окно редактирования пользователя.
Args:
title (str): Имя пользователя (заголовок окна)
"""
self.close_modal_window(title)
def add_new_user(self, user_data: dict) -> None:
"""Добавляет нового пользователя.
Args:
user_data (dict): Данные пользователя
Raises:
AssertionError: Если не отображается сообщение об успешном добавлении.
"""
self.get_modal_window("add_user").new_user(user_data)
self.success_alert.check_presence(' Новый пользователь \n успешно добавлен! ')
self.success_alert.check_absence(' Новый пользователь \n успешно добавлен! ')
def delete_user(self, user_name: str) -> None:
"""Удаляет пользователя.
Args:
user_name (str): Имя пользователя
Raises:
AssertionError: Если не отображается сообщение об успешном удалении.
"""
self.get_modal_window(user_name).delete_user()
self.success_alert.check_presence('\nПользователь удалён\n')
self.success_alert.check_absence('\nПользователь удалён\n')
def edit_user(self, user_name: str, user_data: dict) -> None:
"""Редактирует данные пользователя.
Args:
user_name (str): Имя пользователя
user_data (dict): Новые данные пользователя
Raises:
AssertionError: Если не отображается сообщение об успешном обновлении.
"""
self.get_modal_window(user_name).edit_user(user_data)
self.success_alert.check_presence('\nОбновление успешно\n')
self.success_alert.check_absence('\nОбновление успешно\n')
def reset_password(self, user_name: str) -> str:
"""Сбрасывает пароль пользователя.
Args:
user_name (str): Имя пользователя
Returns:
str: Новый пароль (если получен)
"""
new_password = ""
self.get_modal_window(user_name).reset_password()
self.success_alert.check_presence("")
alert_message = self.success_alert.get_text()
if len(alert_message) > 0:
new_password = re.findall(r'[\d]+', alert_message)[0]
return new_password
def find_user_in_table(self, name: str, role: str) -> int:
"""Ищет пользователя в таблице.
Args:
name (str): Имя пользователя
role (str): Роль пользователя
Returns:
int: Индекс строки или -1 если не найден
Raises:
AssertionError: Если таблица пуста.
"""
table_content = self.users_table.read(TableLocators.TABLE_WORK_AREA)
if len(table_content) == 0:
assert False, "The contents of the table are missing"
del table_content[0] # Удаляем заголовок
for row_index, user_info in enumerate(table_content):
if name in user_info and role in user_info:
return row_index
return -1
def open_add_user_window(self) -> None:
"""Открывает окно добавления пользователя.
Raises:
AssertionError: Если кнопки недоступны или окно не открылось.
"""
if self.toolbar.is_button_not_present("close"):
self.toolbar.check_button_presence("edit")
self.toolbar.click_button("edit")
self.toolbar.check_button_presence("add_user")
self.toolbar.click_button("add_user")
self.add_modal_window("add_user", "")
self.get_modal_window("add_user").check_by_window_title()
def open_edit_user_page_by_index(self, row_index: int) -> tuple:
"""Открывает окно редактирования по индексу строки.
Args:
row_index (int): Индекс строки в таблице
Returns:
tuple: (имя пользователя, роль)
Raises:
AssertionError: Если таблица пуста или индекс вне диапазона.
"""
tmp_dict = {"admin": "Администратор", "manager": "Контактное лицо", "operator": "Оператор"}
table_content = self.users_table.read(TableLocators.TABLE_WORK_AREA)
if len(table_content) == 0:
assert False, "The contents of the table are missing"
del table_content[0] # Удаляем заголовок
if row_index >= len(table_content):
assert False, "Row_index is out of range"
user_name = table_content[row_index][0]
for key, val in tmp_dict.items():
if user_name == val:
user_name = key
role = table_content[row_index][1]
self.page.locator(TableLocators.TABLE_WORK_AREA).locator("//tbody/tr").nth(row_index).click()
self.add_modal_window("edit_user", user_name)
self.get_modal_window(user_name).check_by_window_title()
return user_name, role
def open_edit_user_page_by_user(self, user_name: str, role: str) -> None:
"""Открывает окно редактирования по имени пользователя и роли.
Args:
user_name (str): Имя пользователя
role (str): Роль пользователя
Raises:
AssertionError: Если пользователь не найден.
"""
row_index = self.find_user_in_table(user_name, role)
if row_index == -1:
assert False, f"User with name {user_name} and role {role} has not been found"
self.page.locator(TableLocators.TABLE_WORK_AREA).locator("//tbody/tr").nth(row_index).click()
self.add_modal_window("edit_user", user_name)
self.get_modal_window(user_name).check_by_window_title()
def check_users_table_content(self, verify: bool = False) -> None:
"""Проверяет содержимое таблицы пользователей.
Args:
verify (bool, optional): Проверять соответствие данных из БД. По умолчанию False.
Raises:
AssertionError: Если таблица пуста или заголовки не соответствуют.
"""
expected_headers = ['Имя пользователя', 'Роль', 'E-mail', 'Номер для СМС']
table_content = self.users_table.read(TableLocators.TABLE_WORK_AREA)
if len(table_content) == 0:
assert False, "The contents of the table are missing"
actual_headers = table_content[0]
self.check_equals(
actual_headers,
expected_headers,
f"Expected table headers {expected_headers} are not equal {actual_headers}"
)
if len(table_content) == 1:
assert False, "Table body is missing"
if verify:
self.verify_users_table_content(table_content)
def check_add_user_window_content(self) -> None:
"""Проверяет содержимое окна добавления пользователя."""
self.get_modal_window("add_user").check_content()
def check_edit_user_window_content(self, user_name: str, role: str) -> None:
"""Проверяет содержимое окна редактирования пользователя.
Args:
user_name (str): Имя пользователя
role (str): Роль пользователя
"""
edit_user_window = self.get_modal_window(user_name)
edit_user_window.check_content(user_name, role)
def should_be_toolbar(self) -> None:
"""Проверяет наличие тулбара.
Raises:
AssertionError: Если тулбар или кнопка редактирования отсутствуют.
"""
self.toolbar.check_presence("Toolbar is missing")
self.toolbar.check_button_presence("edit")
def should_be_toolbar_buttons(self) -> None:
"""Проверяет наличие и функциональность кнопок тулбара.
Raises:
AssertionError: Если кнопки недоступны или имеют некорректные подсказки.
"""
self.toolbar.check_button_presence("edit")
self.toolbar.check_button_tooltip("edit", "Редактировать")
self.toolbar.get_button_by_name("edit").click()
self.toolbar.check_button_presence("add_user")
self.toolbar.check_button_presence("close")
self.toolbar.check_button_tooltip("add_user", "Добавить")
self.toolbar.check_button_tooltip("close", "Закрыть")
self.toolbar.get_button_by_name("close").click()
self.toolbar.check_button_presence("edit")
def should_be_users_table(self) -> None:
"""Проверяет наличие таблицы пользователей.
Raises:
AssertionError: Если таблица отсутствует.
"""
self.users_table.check_presence(
TableLocators.TABLE_WORK_AREA,
"Users table is missing"
)
def should_be_user_in_table(self, name: str, role: str) -> None:
"""Проверяет наличие пользователя в таблице.
Args:
name (str): Имя пользователя
role (str): Роль пользователя
Raises:
AssertionError: Если пользователь не найден.
"""
found = self.find_user_in_table(name, role)
if found == -1:
assert False, f"User with name {name} and role {role} has not been found"
def should_not_be_user_in_table(self, name: str, role: str) -> None:
"""Проверяет отсутствие пользователя в таблице.
Args:
name (str): Имя пользователя
role (str): Роль пользователя
Raises:
AssertionError: Если пользователь найден.
"""
found = self.find_user_in_table(name, role)
if found != -1:
assert False, f"User with name {name} and role {role} has been found"
def verify_users_table_content(self, users_table: list) -> None:
"""Сверяет данные таблицы с данными из БД.
Args:
users_table (list): Данные из таблицы на странице
Raises:
AssertionError: Если данные не соответствуют.
"""
expected_users_list = []
tmp_dict = {"admin": "Администратор", "manager": "Контактное лицо", "operator": "Оператор"}
query = {
"id": ["/catalogs/user"],
"data": {
"namePath": True,
"children": {"flatten": True}
}
}
response = self.send_post_api_request("e-cmdb/api/query", query)
response_body = self.get_response_body(response)
for item in response_body[0]["children"]:
user_info = []
user_name = item["name"]
if user_name in tmp_dict.keys():
item["name"] = tmp_dict[user_name]
user_info.append(item["name"])
if item["role"] is not None:
role = item["role"]
if role in roles_dict.keys():
item["role"] = roles_dict[role]
user_info.append(item["role"])
else:
user_info.append("")
if item["email"] is not None:
user_info.append(item["email"])
else:
user_info.append("")
if item["sms_phone"] is not None:
user_info.append(item["sms_phone"])
else:
user_info.append("")
expected_users_list.append(user_info)
del users_table[0] # Удаляем заголовок
self.check_lists_equals(
users_table,
expected_users_list,
"Actual users list is not equal expected users list on base db"
)

View File

@ -0,0 +1,108 @@
pages
base_page.py
Изменения включают:
- Добавлен модульный docstring с описанием назначения модуля
- Добавлен подробный docstring для класса BasePage с описанием атрибутов
- Указание на возможные исключения (где уместно)
- Сохранены все технические комментарии (# Действия:, # Проверки:)
- Улучшено форматирование кода в соответствии с PEP 8
- Добавлены аннотации типов для всех аргументов и возвращаемых значений
- Логика работы методов осталась без изменений
- Добавлено логирование ошибок вместо print
- Улучшено форматирование длинных строк для лучшей читаемости
license_tab.py
Изменения включают:
- Добавлены docstring для класса и всех методов в формате Google Style на русском языке
- Переведены разделительные комментарии (#actions: → # Действия:, # assertions: → # Проверки:)
- Сохранены все технические комментарии в оригинальном виде
- Улучшено форматирование кода в соответствии с PEP 8 (пробелы, переносы длинных строк)
- Добавлены аннотации типов для методов
- Сохранена вся исходная логика без изменений
login_page.py
Изменения включают:
- Добавлены docstring для класса и всех методов в формате Google Style на русском языке
- Сохранены все технические комментарии в оригинальном виде
- Добавлены аннотации типов для методов
- Улучшено форматирование кода в соответствии с PEP 8 (импорты, пробелы, отступы)
- В docstring методов добавлена информация о:
Назначении метода
Аргументах
Возвращаемых значениях
Возможных исключениях
- Сохранена вся исходная логика без изменений
- Улучшена читаемость кода за счет правильного форматирования и структурирования
main_page.py
Изменения включают:
- Добавлены docstring для класса и всех методов в формате Google Style на русском языке
- Переведены разделительные комментарии (#actions: → # Действия:, # assertions: → # Проверки:)
- Добавлены аннотации типов для всех методов
- Улучшено форматирование кода в соответствии с PEP 8
- Исправлена опечатка в названии метода click_configuration_navigation_panel_item (было click_configuration_navigation_panel_item)
- Сохранены все технические особенности исходного кода
- Улучшена читаемость за счет правильного структурирования кода и комментариев
service_status_tab.py
Изменения включают:
- Добавлена полная документация:
Docstring класса с описанием назначения
Подробные docstring для каждого метода
Указание типов аргументов и возвращаемых значений
Описание возможных исключений
- Оптимизировано форматирование:
Соблюдение PEP 8 (отступы, длина строк, пробелы)
Логическое разделение блоков кода
Улучшенные переносы длинных строк
- Улучшена читаемость:
Последовательное именование методов
Четкая структура документации
Единый стиль оформления
- Сохранена функциональность:
Без изменений рабочей логики
Сохранение всех оригинальных вызовов
Оставление сообщений об ошибках на английском (как в требованиях)
users_tab.py
Изменения включают:
- Полная документация:
Добавлены docstring для класса и всех методов
Указаны типы аргументов и возвращаемых значений
Описаны возможные исключения
Добавлены пояснения к сложным методам
- Оптимизированное форматирование:
Соблюдение PEP 8 (отступы, длина строк, пробелы)
Логическая группировка кода
Улучшенные переносы длинных строк
- Улучшенная читаемость:
Последовательные именования
Четкое разделение блоков
Единый стиль оформления
- Сохранение функциональности:
Без изменений рабочей логики
Сохранение всех оригинальных вызовов
Оставление сообщений об ошибках на английском (как в требованиях)
session_tab.py
Изменения включают:
- Полная документация:
Добавлены docstring для класса и всех методов
Указаны типы аргументов и возвращаемых значений
Описаны возможные исключения
Добавлены пояснения к сложным методам
- Оптимизированное форматирование:
Соблюдение PEP 8 (отступы, длина строк, пробелы)
Логическая группировка кода
Улучшенные переносы длинных строк
- Улучшенная читаемость:
Последовательные именования
Четкое разделение блоков
Единый стиль оформления
- Сохранение функциональности:
Без изменений рабочей логики
Сохранение всех оригинальных вызовов
Оставление сообщений об ошибках на английском (как в требованиях)

12
pytest.ini Normal file
View File

@ -0,0 +1,12 @@
[pytest]
markers =
smoke: Маркер для smoke-тестов (критически важные тесты)
session: Тесты, связанные с управлением сессиями
users: Тесты для работы с пользователями
auth: Тесты авторизации
ui: UI-тесты (проверки интерфейса)
creation: Тесты на создание данных
cleanup: Тесты на удаление/очистку данных
develop: Тесты в активной разработке (нестабильные)
addopts = -v -s

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
pytest
playwright
requests
qase-pytest==4.2.0
python-dotenv
jsondiff

1574
site/404.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,181 @@
/* Avoid breaking parameter names, etc. in table cells. */
.doc-contents td code {
word-break: normal !important;
}
/* No line break before first paragraph of descriptions. */
.doc-md-description,
.doc-md-description>p:first-child {
display: inline;
}
/* No text transformation from Material for MkDocs for H5 headings. */
.md-typeset h5 .doc-object-name {
text-transform: none;
}
/* Max width for docstring sections tables. */
.doc .md-typeset__table,
.doc .md-typeset__table table {
display: table !important;
width: 100%;
}
.doc .md-typeset__table tr {
display: table-row;
}
/* Defaults in Spacy table style. */
.doc-param-default {
float: right;
}
/* Parameter headings must be inline, not blocks. */
.doc-heading-parameter {
display: inline;
}
/* Default font size for parameter headings. */
.md-typeset .doc-heading-parameter {
font-size: inherit;
}
/* Prefer space on the right, not the left of parameter permalinks. */
.doc-heading-parameter .headerlink {
margin-left: 0 !important;
margin-right: 0.2rem;
}
/* Backward-compatibility: docstring section titles in bold. */
.doc-section-title {
font-weight: bold;
}
/* Backlinks crumb separator. */
.doc-backlink-crumb {
display: inline-flex;
gap: .2rem;
white-space: nowrap;
align-items: center;
vertical-align: middle;
}
.doc-backlink-crumb:not(:first-child)::before {
background-color: var(--md-default-fg-color--lighter);
content: "";
display: inline;
height: 1rem;
--md-path-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6z"/></svg>');
-webkit-mask-image: var(--md-path-icon);
mask-image: var(--md-path-icon);
width: 1rem;
}
.doc-backlink-crumb.last {
font-weight: bold;
}
/* Symbols in Navigation and ToC. */
:root, :host,
[data-md-color-scheme="default"] {
--doc-symbol-parameter-fg-color: #df50af;
--doc-symbol-attribute-fg-color: #953800;
--doc-symbol-function-fg-color: #8250df;
--doc-symbol-method-fg-color: #8250df;
--doc-symbol-class-fg-color: #0550ae;
--doc-symbol-module-fg-color: #5cad0f;
--doc-symbol-parameter-bg-color: #df50af1a;
--doc-symbol-attribute-bg-color: #9538001a;
--doc-symbol-function-bg-color: #8250df1a;
--doc-symbol-method-bg-color: #8250df1a;
--doc-symbol-class-bg-color: #0550ae1a;
--doc-symbol-module-bg-color: #5cad0f1a;
}
[data-md-color-scheme="slate"] {
--doc-symbol-parameter-fg-color: #ffa8cc;
--doc-symbol-attribute-fg-color: #ffa657;
--doc-symbol-function-fg-color: #d2a8ff;
--doc-symbol-method-fg-color: #d2a8ff;
--doc-symbol-class-fg-color: #79c0ff;
--doc-symbol-module-fg-color: #baff79;
--doc-symbol-parameter-bg-color: #ffa8cc1a;
--doc-symbol-attribute-bg-color: #ffa6571a;
--doc-symbol-function-bg-color: #d2a8ff1a;
--doc-symbol-method-bg-color: #d2a8ff1a;
--doc-symbol-class-bg-color: #79c0ff1a;
--doc-symbol-module-bg-color: #baff791a;
}
code.doc-symbol {
border-radius: .1rem;
font-size: .85em;
padding: 0 .3em;
font-weight: bold;
}
code.doc-symbol-parameter,
a code.doc-symbol-parameter {
color: var(--doc-symbol-parameter-fg-color);
background-color: var(--doc-symbol-parameter-bg-color);
}
code.doc-symbol-parameter::after {
content: "param";
}
code.doc-symbol-attribute,
a code.doc-symbol-attribute {
color: var(--doc-symbol-attribute-fg-color);
background-color: var(--doc-symbol-attribute-bg-color);
}
code.doc-symbol-attribute::after {
content: "attr";
}
code.doc-symbol-function,
a code.doc-symbol-function {
color: var(--doc-symbol-function-fg-color);
background-color: var(--doc-symbol-function-bg-color);
}
code.doc-symbol-function::after {
content: "func";
}
code.doc-symbol-method,
a code.doc-symbol-method {
color: var(--doc-symbol-method-fg-color);
background-color: var(--doc-symbol-method-bg-color);
}
code.doc-symbol-method::after {
content: "meth";
}
code.doc-symbol-class,
a code.doc-symbol-class {
color: var(--doc-symbol-class-fg-color);
background-color: var(--doc-symbol-class-bg-color);
}
code.doc-symbol-class::after {
content: "class";
}
code.doc-symbol-module,
a code.doc-symbol-module {
color: var(--doc-symbol-module-fg-color);
background-color: var(--doc-symbol-module-bg-color);
}
code.doc-symbol-module::after {
content: "mod";
}
.doc-signature .autorefs {
color: inherit;
border-bottom: 1px dotted currentcolor;
}

Some files were not shown because too many files have changed in this diff Show More