Compare commits
22 Commits
ra5/create
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
e3804bd9c9 | |
|
|
a07cb43b80 | |
|
|
06e680675c | |
|
|
18f7873145 | |
|
|
e926b04a14 | |
|
|
57d8a0466d | |
|
|
cab4f18f55 | |
|
|
bfb4082a2d | |
|
|
db50e80c51 | |
|
|
82a28dda72 | |
|
|
085d8c4ec7 | |
|
|
384ee4e15e | |
|
|
036f86efad | |
|
|
e1e166b878 | |
|
|
713cfd6126 | |
|
|
f075024386 | |
|
|
b024fac0d8 | |
|
|
0295852986 | |
|
|
4fff4835f1 | |
|
|
ca7c69c423 | |
|
|
0509d5bee3 | |
|
|
afb611dae9 |
|
|
@ -0,0 +1,3 @@
|
||||||
|
ENV=test
|
||||||
|
AUTH_LOGIN = admin
|
||||||
|
AUTH_PASSWORD = enodemon-admin
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
"""Модуль компонента группы чек-боксов.
|
||||||
|
|
||||||
|
Содержит класс CheckboxGroupComponent для работы с группами чек-боксов,
|
||||||
|
в том числе в выпадающих списках с множественным выбором.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from playwright.sync_api import Page, Locator, expect
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from components.base_component import BaseComponent
|
||||||
|
|
||||||
|
logger = get_logger("CHECKBOX_GROUP_COMPONENT")
|
||||||
|
|
||||||
|
class CheckboxGroupComponent(BaseComponent):
|
||||||
|
"""Компонент для работы с группами чек-боксов.
|
||||||
|
|
||||||
|
Позволяет выбирать/снимать выбор с чек-боксов в группе,
|
||||||
|
получать список выбранных элементов и проверять их состояние.
|
||||||
|
Может использоваться как для выпадающих списков с множественным выбором,
|
||||||
|
так и для любых других групп чек-боксов на странице.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, page: Page) -> None:
|
||||||
|
"""Инициализирует компонент группы чек-боксов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Экземпляр страницы Playwright.
|
||||||
|
"""
|
||||||
|
super().__init__(page)
|
||||||
|
|
||||||
|
def get_checkbox_locator(self, text: str, container_locator: Locator | None = None) -> Locator:
|
||||||
|
"""Возвращает локатор чек-бокса с указанным текстом.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Текст элемента для выбора.
|
||||||
|
container_locator (Locator | None): Локатор контейнера с чек-боксами.
|
||||||
|
Если не указан, поиск по всей странице.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Locator: Локатор чек-бокса.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if container_locator:
|
||||||
|
listitem_locator = container_locator.get_by_role("listitem")
|
||||||
|
else:
|
||||||
|
listitem_locator = self.page.locator("//div[contains(@class, 'menuable__content__active')]"). \
|
||||||
|
get_by_role("listitem")
|
||||||
|
|
||||||
|
listitem_locator.last.scroll_into_view_if_needed()
|
||||||
|
listitem_locator.last.wait_for(state="visible")
|
||||||
|
|
||||||
|
all_items = listitem_locator.all()
|
||||||
|
for i, item in enumerate(all_items):
|
||||||
|
if item.inner_text() == text:
|
||||||
|
checkbox_locator = item.get_by_role("checkbox")
|
||||||
|
expect(checkbox_locator).to_be_visible(), \
|
||||||
|
f"Checkbox with text '{text}' is missing or not visible"
|
||||||
|
return checkbox_locator
|
||||||
|
|
||||||
|
assert False, f"Checkbox locator for {text} has not been found"
|
||||||
|
|
||||||
|
def get_checkbox_locator_or(self, text: str, container_locator: Locator | None = None) -> Locator:
|
||||||
|
"""Возвращает локатор чек-бокса с указанным текстом.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Текст элемента для выбора.
|
||||||
|
container_locator (Locator | None): Локатор контейнера с чек-боксами.
|
||||||
|
Если не указан, поиск по всей странице.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Locator: Локатор чек-бокса.
|
||||||
|
"""
|
||||||
|
if container_locator:
|
||||||
|
checkbox_locator = container_locator.get_by_role("listitem").filter(has_text=text).get_by_role("checkbox")
|
||||||
|
else:
|
||||||
|
checkbox_locator = self.page.get_by_role("listitem").filter(has_text=text).get_by_role("checkbox")
|
||||||
|
|
||||||
|
if checkbox_locator.count() > 1:
|
||||||
|
rtext = f"^{text}$"
|
||||||
|
if container_locator:
|
||||||
|
checkbox_locator = container_locator.get_by_role("listitem").filter(
|
||||||
|
has_text=re.compile(rtext)
|
||||||
|
).get_by_role("checkbox")
|
||||||
|
else:
|
||||||
|
checkbox_locator = self.page.get_by_role("listitem").filter(
|
||||||
|
has_text=re.compile(rtext)
|
||||||
|
).get_by_role("checkbox")
|
||||||
|
|
||||||
|
expect(checkbox_locator).to_be_visible(), \
|
||||||
|
f"Checkbox with text '{text}' is missing or not visible"
|
||||||
|
return checkbox_locator
|
||||||
|
|
||||||
|
def uncheck_by_text(self, text: str, container_locator: Locator | None = None) -> None:
|
||||||
|
"""Снимает выбор с чек-бокса по указанному тексту.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Текст чек-бокса для снятия выбора.
|
||||||
|
container_locator (Locator | None): Локатор контейнера с чек-боксами.
|
||||||
|
"""
|
||||||
|
logger.info(f"Unchecking checkbox with text: {text}")
|
||||||
|
self.get_checkbox_locator(text, container_locator).uncheck(force=True)
|
||||||
|
|
||||||
|
def check_by_text(self, text: str, container_locator: Locator | None = None) -> None:
|
||||||
|
"""Выбирает чек-бокс по указанному тексту.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Текст чек-бокса для выбора.
|
||||||
|
container_locator (Locator | None): Локатор контейнера с чек-боксами.
|
||||||
|
"""
|
||||||
|
logger.info(f"Checking checkbox with text: {text}")
|
||||||
|
self.get_checkbox_locator(text, container_locator).check(force=True)
|
||||||
|
|
||||||
|
def get_checked_items(self, container_locator: str | Locator) -> list[str]:
|
||||||
|
"""Возвращает список текстов отмеченных чек-боксов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_locator (str | Locator): Локатор контейнера с группой чек-боксов.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: Список текстов выбранных чек-боксов.
|
||||||
|
"""
|
||||||
|
checked_items = []
|
||||||
|
list_container = self.get_locator(container_locator)
|
||||||
|
items = list_container.get_by_role("listitem").all()
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if item.get_by_role("checkbox").is_checked():
|
||||||
|
item_text = item.text_content().strip()
|
||||||
|
if item_text:
|
||||||
|
checked_items.append(item_text)
|
||||||
|
|
||||||
|
logger.info(f"Checked items: {checked_items}")
|
||||||
|
return checked_items
|
||||||
|
|
||||||
|
def are_items_checked(self, container_locator: str | Locator, expected_items: list[str]) -> bool:
|
||||||
|
"""Проверяет, что указанные чек-боксы выбраны.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_locator (str | Locator): Локатор контейнера с группой чек-боксов.
|
||||||
|
expected_items (list[str]): Список ожидаемых выбранных элементов.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если все указанные чек-боксы выбраны.
|
||||||
|
"""
|
||||||
|
checked_items = self.get_checked_items(container_locator)
|
||||||
|
return all(item in checked_items for item in expected_items)
|
||||||
|
|
||||||
|
def check_all(self, container_locator: str | Locator) -> None:
|
||||||
|
"""Выбирает все чек-боксы в группе.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_locator (str | Locator): Локатор контейнера с группой чек-боксов.
|
||||||
|
"""
|
||||||
|
logger.info("Checking all checkboxes in group")
|
||||||
|
list_container = self.get_locator(container_locator)
|
||||||
|
checkboxes = list_container.get_by_role("checkbox").all()
|
||||||
|
|
||||||
|
for checkbox in checkboxes:
|
||||||
|
if not checkbox.is_checked():
|
||||||
|
checkbox.check(force=True)
|
||||||
|
|
||||||
|
def uncheck_all(self, container_locator: str | Locator) -> None:
|
||||||
|
"""Снимает выбор со всех чек-боксов в группе.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_locator (str | Locator): Локатор контейнера с группой чек-боксов.
|
||||||
|
"""
|
||||||
|
logger.info("Unchecking all checkboxes in group")
|
||||||
|
list_container = self.get_locator(container_locator)
|
||||||
|
checkboxes = list_container.get_by_role("checkbox").all()
|
||||||
|
|
||||||
|
for checkbox in checkboxes:
|
||||||
|
if checkbox.is_checked():
|
||||||
|
checkbox.uncheck(force=True)
|
||||||
|
|
||||||
|
def get_items_count(self, container_locator: str | Locator) -> int:
|
||||||
|
"""Возвращает количество чек-боксов в группе.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_locator (str | Locator): Локатор контейнера с группой чек-боксов.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Количество чек-боксов.
|
||||||
|
"""
|
||||||
|
list_container = self.get_locator(container_locator)
|
||||||
|
return list_container.get_by_role("checkbox").count()
|
||||||
|
|
@ -86,7 +86,7 @@ class DatePickerComponent(BaseComponent):
|
||||||
|
|
||||||
days_table_locator = self.page.locator(DatePickerLocators.DATE_PICKER_TABLE_DAYS)
|
days_table_locator = self.page.locator(DatePickerLocators.DATE_PICKER_TABLE_DAYS)
|
||||||
days_table_locator.wait_for(timeout=300)
|
days_table_locator.wait_for(timeout=300)
|
||||||
day_button_locator = days_table_locator.locator("//td").get_by_role("button", name=day)
|
day_button_locator = days_table_locator.locator("//td").get_by_role("button", name=day, exact=True)
|
||||||
visible = day_button_locator.is_visible()
|
visible = day_button_locator.is_visible()
|
||||||
if visible:
|
if visible:
|
||||||
day_button_locator.click()
|
day_button_locator.click()
|
||||||
|
|
@ -189,7 +189,7 @@ class DatePickerComponent(BaseComponent):
|
||||||
assert actual_month_year == expected_month_year, \
|
assert actual_month_year == expected_month_year, \
|
||||||
f"Expected value {expected_month_year} is not equal actual value {actual_month_year} on date picker body"
|
f"Expected value {expected_month_year} is not equal actual value {actual_month_year} on date picker body"
|
||||||
|
|
||||||
expected_day = str(expected_date.strftime("%d"))
|
expected_day = str(expected_date.strftime("%d")).lstrip('0')
|
||||||
actual_day = self.get_day()
|
actual_day = self.get_day()
|
||||||
assert actual_day == expected_day, \
|
assert actual_day == expected_day, \
|
||||||
f"Expected day {expected_day} is not equal actual day {actual_day} on date picker body"
|
f"Expected day {expected_day} is not equal actual day {actual_day} on date picker body"
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ class EventsContainerComponent(BaseComponent):
|
||||||
self.last_page.click()
|
self.last_page.click()
|
||||||
|
|
||||||
def click_filter_button(self) -> EventsFilterPanel:
|
def click_filter_button(self) -> EventsFilterPanel:
|
||||||
"""Нажатие кнопки перехода на первую страницу"""
|
"""Нажатие кнопки фильтр"""
|
||||||
|
|
||||||
self.toolbar.click_button("filter_button")
|
self.toolbar.click_button("filter_button")
|
||||||
expect(self.page.locator("div.menuable__content__active")).to_be_visible(), "Events filter is missing"
|
expect(self.page.locator("div.menuable__content__active")).to_be_visible(), "Events filter is missing"
|
||||||
|
|
@ -207,6 +207,16 @@ class EventsContainerComponent(BaseComponent):
|
||||||
|
|
||||||
self.events_table.check_table_headers(actual_headers, expected_headers)
|
self.events_table.check_table_headers(actual_headers, expected_headers)
|
||||||
|
|
||||||
|
def check_events_table_status_button(self, row_index: int, tooltip_text: str) -> None:
|
||||||
|
""" Проверка наличия в строке кнопки статуса (состояния) и ее тултипа"""
|
||||||
|
|
||||||
|
loc = self.container_locator.locator(self.table_locator)
|
||||||
|
row_locator = self.events_table.get_row_locator(loc, row_index-1)
|
||||||
|
button = TooltipButton(self.page, row_locator.get_by_role("button"), "status_button")
|
||||||
|
button.check_visibility(f"Tooltip button is missing in {row_index} table row")
|
||||||
|
button.check_tooltip_with_text(tooltip_text)
|
||||||
|
|
||||||
|
|
||||||
def check_events_table_column_descending_order(self,
|
def check_events_table_column_descending_order(self,
|
||||||
index: int,
|
index: int,
|
||||||
convert2timestamp=False) -> bool:
|
convert2timestamp=False) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -252,12 +252,13 @@ class NavigationPanelComponent(BaseComponent):
|
||||||
assert False, "Workarea already reduced"
|
assert False, "Workarea already reduced"
|
||||||
|
|
||||||
# Проверки:
|
# Проверки:
|
||||||
def check_item_visibility(self, locator: str | Locator, item_name: str) -> None:
|
def check_item_visibility(self, locator: str | Locator, item_name: str, parent = None) -> None:
|
||||||
"""Проверяет видимость элемента с указанным текстом.
|
"""Проверяет видимость элемента с указанным текстом.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
locator: Локатор элемента или строка с CSS/XPath.
|
locator: Локатор элемента или строка с CSS/XPath.
|
||||||
item_name: Текст элемента для проверки.
|
item_name: Текст элемента для проверки.
|
||||||
|
parent: Текст родительского элемента (необязательный параметр)
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
Временная обработка для элементов с текстом 'Шаблоны'.
|
Временная обработка для элементов с текстом 'Шаблоны'.
|
||||||
|
|
@ -265,17 +266,13 @@ class NavigationPanelComponent(BaseComponent):
|
||||||
|
|
||||||
msg = f"Navigation panel item '{item_name}' is not visible"
|
msg = f"Navigation panel item '{item_name}' is not visible"
|
||||||
|
|
||||||
## временно: в навигационной панели есть две панели с именем Шаблоны
|
|
||||||
## для их различия добавлены индексы Шаблоны_1 для Настройки/Шаблоны
|
|
||||||
## Шаблоны_2 для Настройки/ZTP/Шаблоны
|
|
||||||
loc = self.get_locator(locator)
|
loc = self.get_locator(locator)
|
||||||
if item_name == "Шаблоны_1":
|
if parent:
|
||||||
loc = loc.get_by_text("Шаблоны").first
|
parent_loc = f"//div[contains(@class, 'v-treeview-node') and contains(.,'{parent}')]"
|
||||||
elif item_name == "Шаблоны_2":
|
loc = loc.locator(parent_loc)
|
||||||
loc = loc.get_by_text("Шаблоны").nth(1)
|
item_loc = loc.get_by_text(item_name).first
|
||||||
else:
|
|
||||||
loc = loc.get_by_text(item_name)
|
self.check_visibility(item_loc, msg)
|
||||||
self.check_visibility(loc, msg)
|
|
||||||
|
|
||||||
def is_item_visible(self, locator: str | Locator, item_name: str) -> bool:
|
def is_item_visible(self, locator: str | Locator, item_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""Модуль компонента тулбара (class=toolbar_castom).
|
||||||
|
|
||||||
|
Содержит класс ToolbarComponent для работы с элементами тулбара
|
||||||
|
- Проверка видимости элементов
|
||||||
|
"""
|
||||||
|
|
||||||
|
from playwright.sync_api import Page, expect
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from locators.certificate_locators import CertificateLocators
|
||||||
|
from components.base_component import BaseComponent
|
||||||
|
|
||||||
|
logger = get_logger("TOOLBAR_CUSTOM")
|
||||||
|
|
||||||
|
class CustomToolbar(BaseComponent):
|
||||||
|
"""Класс для работы с информационным тулбаром на странице.
|
||||||
|
|
||||||
|
Наследует функциональность BaseComponent и добавляет специфичные
|
||||||
|
методы и проверки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, page: Page) -> None:
|
||||||
|
"""Инициализирует компонент тулбара."""
|
||||||
|
|
||||||
|
super().__init__(page)
|
||||||
|
|
||||||
|
# Действия:
|
||||||
|
# (Методы действий будут добавлены по мере необходимости)
|
||||||
|
|
||||||
|
# Проверки:
|
||||||
|
def check_toolbar_presence(self, titles: list[str]) -> None:
|
||||||
|
"""Проверяет видимость тулбара.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
titles: Набор заголовков тулбара
|
||||||
|
"""
|
||||||
|
|
||||||
|
for title in titles:
|
||||||
|
locator = self.page.locator(f"{CertificateLocators.TOOLBAR_CASTOM}//span[contains(text(),'{title}')]")
|
||||||
|
expect(locator).to_be_visible(), f"Toolbar with title {title} is not visible"
|
||||||
|
|
@ -114,8 +114,16 @@ class ActionsEventsContainer(EventsContainerComponent):
|
||||||
|
|
||||||
self.check_events_table_headers(events_table[0], expected_headers)
|
self.check_events_table_headers(events_table[0], expected_headers)
|
||||||
|
|
||||||
|
for i in range(len(expected_headers)):
|
||||||
|
actual_state = self.get_arrow_button_state(i)
|
||||||
|
assert actual_state == "down", f"Arrow state for column {i} should be 'down'"
|
||||||
|
|
||||||
if len(events_table) == 1:
|
if len(events_table) == 1:
|
||||||
logger.info("Table body is missing")
|
logger.info("Table body is missing")
|
||||||
|
else:
|
||||||
|
rows_count = len(events_table)
|
||||||
|
for j in range(1, rows_count-1):
|
||||||
|
self.check_events_table_status_button(j, "Статус")
|
||||||
|
|
||||||
self.should_be_pagination_buttons()
|
self.should_be_pagination_buttons()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@ class AuditEventsContainer(EventsContainerComponent):
|
||||||
assert False, "The contents of the events table are missing"
|
assert False, "The contents of the events table are missing"
|
||||||
|
|
||||||
self.check_events_table_headers(events_table[0], expected_headers)
|
self.check_events_table_headers(events_table[0], expected_headers)
|
||||||
|
for i in range(len(expected_headers)):
|
||||||
|
actual_state = self.get_arrow_button_state(i)
|
||||||
|
assert actual_state == "down", f"Arrow state for column {i} should be 'down'"
|
||||||
|
|
||||||
if len(events_table) == 1:
|
if len(events_table) == 1:
|
||||||
logger.info("Table body is missing")
|
logger.info("Table body is missing")
|
||||||
|
|
@ -97,6 +100,9 @@ class AuditEventsContainer(EventsContainerComponent):
|
||||||
assert False, "The contents of the events table are missing"
|
assert False, "The contents of the events table are missing"
|
||||||
|
|
||||||
self.check_events_table_headers(events_table[0], expected_headers)
|
self.check_events_table_headers(events_table[0], expected_headers)
|
||||||
|
for i in range(len(expected_headers)):
|
||||||
|
actual_state = self.get_arrow_button_state(i)
|
||||||
|
assert actual_state == "down", f"Arrow state for column {i} should be 'down'"
|
||||||
|
|
||||||
if len(events_table) == 1:
|
if len(events_table) == 1:
|
||||||
logger.info("Table body is missing")
|
logger.info("Table body is missing")
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ class EventsTabContainer(EventsContainerComponent):
|
||||||
assert False, "The contents of the events table are missing"
|
assert False, "The contents of the events table are missing"
|
||||||
|
|
||||||
self.check_events_table_headers(events_table[0], expected_headers)
|
self.check_events_table_headers(events_table[0], expected_headers)
|
||||||
|
for i in range(len(expected_headers)):
|
||||||
|
actual_state = self.get_arrow_button_state(i)
|
||||||
|
assert actual_state == "down", f"Arrow state for column {i} should be 'down'"
|
||||||
|
|
||||||
if len(events_table) == 1:
|
if len(events_table) == 1:
|
||||||
logger.info("Table body is missing")
|
logger.info("Table body is missing")
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,19 @@ class MaintenanceEventsContainer(EventsContainerComponent):
|
||||||
assert False, "The contents of the events table are missing"
|
assert False, "The contents of the events table are missing"
|
||||||
|
|
||||||
self.check_events_table_headers(events_table[0], expected_headers)
|
self.check_events_table_headers(events_table[0], expected_headers)
|
||||||
|
for i in range(len(expected_headers)):
|
||||||
|
actual_state = self.get_arrow_button_state(i)
|
||||||
|
assert actual_state == "down", f"Arrow state for column {i} should be 'down'"
|
||||||
|
|
||||||
if len(events_table) == 1:
|
rows_count = len(events_table)
|
||||||
|
|
||||||
|
if rows_count == 1:
|
||||||
logger.info("Table body is missing")
|
logger.info("Table body is missing")
|
||||||
|
else:
|
||||||
|
j = 1
|
||||||
|
while j < rows_count:
|
||||||
|
self.check_events_table_status_button(j, "Состояние")
|
||||||
|
j += 1
|
||||||
|
|
||||||
self.should_be_pagination_buttons()
|
self.should_be_pagination_buttons()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,21 @@ class SystemLogEventsContainer(EventsContainerComponent):
|
||||||
assert False, "The contents of the events table are missing"
|
assert False, "The contents of the events table are missing"
|
||||||
|
|
||||||
self.check_events_table_headers(events_table[0], expected_headers)
|
self.check_events_table_headers(events_table[0], expected_headers)
|
||||||
|
for i in range(len(expected_headers)):
|
||||||
|
actual_state = self.get_arrow_button_state(i)
|
||||||
|
assert actual_state == "down", f"Arrow state for column {i} should be 'down'"
|
||||||
|
|
||||||
if len(events_table) == 1:
|
if len(events_table) == 1:
|
||||||
logger.info("Table body is missing")
|
logger.info("Table body is missing")
|
||||||
|
else:
|
||||||
|
j = 1
|
||||||
|
# так как записей много, проверяем первые 40
|
||||||
|
rows_count = 40
|
||||||
|
if len(events_table) < 40:
|
||||||
|
rows_count = len(events_table)
|
||||||
|
while j < rows_count:
|
||||||
|
self.check_events_table_status_button(j, "Критичность")
|
||||||
|
j += 1
|
||||||
|
|
||||||
self.should_be_pagination_buttons()
|
self.should_be_pagination_buttons()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ class DateInput(BaseComponent):
|
||||||
result = False
|
result = False
|
||||||
|
|
||||||
inner_text = self.switch_mode_button.get_text(0).strip()
|
inner_text = self.switch_mode_button.get_text(0).strip()
|
||||||
|
print(inner_text)
|
||||||
if inner_text == "keyboard":
|
if inner_text == "keyboard":
|
||||||
result = True
|
result = True
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Модуль контейнера для импорта сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Содержит класс для работы с формой для импорта
|
||||||
|
сертификата во вкладке 'Сертификаты' через Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from playwright.sync_api import Page, expect
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from locators.certificate_locators import CertificateLocators
|
||||||
|
from elements.text_input_element import TextInput
|
||||||
|
from elements.text_element import Text
|
||||||
|
from elements.tooltip_button_element import TooltipButton
|
||||||
|
from components.toolbar_custom_component import CustomToolbar
|
||||||
|
from components.base_component import BaseComponent
|
||||||
|
|
||||||
|
logger = get_logger("IMPORT_CRTIFICATE_FORM")
|
||||||
|
|
||||||
|
|
||||||
|
class ImportCertificateForm(BaseComponent):
|
||||||
|
"""Компонент формы для импорта сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Предоставляет методы для взаимодействия с элементами
|
||||||
|
формы для импорта сертификата во вкладке 'Сертификаты'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, page: Page):
|
||||||
|
"""Инициализирует компонент формы для импорта сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Экземпляр страницы Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(page)
|
||||||
|
|
||||||
|
import_title_locator = page.locator(CertificateLocators.BLOCK_HEADER_TEXT). \
|
||||||
|
filter(has_text='Импорт CA (P12)')
|
||||||
|
self.import_title = Text(page, import_title_locator, "import_title")
|
||||||
|
|
||||||
|
button_locator = page.locator(CertificateLocators.BUTTON_IMPORT)
|
||||||
|
self.button_import = TooltipButton(page, button_locator, "button_import")
|
||||||
|
|
||||||
|
self.toolbar_info = CustomToolbar(page)
|
||||||
|
|
||||||
|
self.password_input = TextInput(page, CertificateLocators.FIELD_INPUT_PASSWORD,
|
||||||
|
"password_input_field")
|
||||||
|
|
||||||
|
# Действия:
|
||||||
|
def get_password_field_value(self) -> str:
|
||||||
|
"""Возвращает текущее значение поля 'Пароль'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str : Текущее значение поля 'Пароль.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.password_input.get_input_value().strip()
|
||||||
|
|
||||||
|
def input_password_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Пароль'"""
|
||||||
|
|
||||||
|
self.password_input.clear()
|
||||||
|
self.password_input.input_value(value)
|
||||||
|
|
||||||
|
def _get_label_for_input_field(self, field_locator: str) -> str:
|
||||||
|
div_loc = f"//div[contains(@class, 'flex')][.{field_locator}]"
|
||||||
|
label = self.page.locator(div_loc).locator("//preceding-sibling::div[1]").locator("//input")
|
||||||
|
return label.input_value()
|
||||||
|
|
||||||
|
# Проверки:
|
||||||
|
def check_content(self):
|
||||||
|
"""Проверяет наличие и корректность всех элементов формы."""
|
||||||
|
|
||||||
|
self.import_title.check_visibility("Title 'Импорт CA (P12)' is missing")
|
||||||
|
|
||||||
|
self.button_import.check_visibility("Import certificate button is missing")
|
||||||
|
assert self.button_import.is_disabled(), "Import certificate button should be disabled"
|
||||||
|
self.button_import.check_tooltip_with_text("Импорт сертификата (CA)")
|
||||||
|
|
||||||
|
# Проверка информационного тулбара
|
||||||
|
self.toolbar_info.check_toolbar_presence(['Создание нового сертификата',
|
||||||
|
'Приведет к замене корневого сертификата системы'])
|
||||||
|
# проверка наличия всех полей формы
|
||||||
|
password_label = self._get_label_for_input_field(CertificateLocators.FIELD_INPUT_PASSWORD).strip()
|
||||||
|
assert password_label == 'Пароль', f"Unexpected field name {password_label} has got"
|
||||||
|
self.password_input.check_visibility("Field password input is missing")
|
||||||
|
|
||||||
|
info_loc = self.page.get_by_text("Пароль используется для расшифровки закрытого ключа в файле P12")
|
||||||
|
expect(info_loc).to_be_visible()
|
||||||
|
|
||||||
|
def is_import_button_disabled(self) -> bool:
|
||||||
|
"""Проверяет наличие и доступность кнопки перевыпуска сертификата."""
|
||||||
|
|
||||||
|
self.button_import.check_visibility("Import certificate button is missing")
|
||||||
|
return self.button_import.is_disabled()
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
"""Модуль interactive_dropdown_list_component содержит класс для работы с интерактивными выпадающими списками,
|
|
||||||
позволяющими сделать выбор нескольких элементов.
|
|
||||||
|
|
||||||
Класс InteractiveDropdownList наследует базовый функционал BaseComponent и добавляет
|
|
||||||
методы для взаимодействия с интерактивными выпадающими списками на странице.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from playwright.sync_api import Page, Locator, expect
|
|
||||||
from tools.logger import get_logger
|
|
||||||
from components.base_component import BaseComponent
|
|
||||||
|
|
||||||
logger = get_logger("INTERACTIVE_DROPDOWN_LIST")
|
|
||||||
|
|
||||||
class InteractiveDropdownList(BaseComponent):
|
|
||||||
"""Класс для работы с выпадающими списками.
|
|
||||||
|
|
||||||
Наследует функциональность BaseElement и добавляет специфичные
|
|
||||||
методы для выбора и проверки элементов списка.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, page: Page) -> None:
|
|
||||||
"""Инициализирует компонент интерактивного выпадающего списка.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
page: Экземпляр страницы Playwright.
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(page)
|
|
||||||
|
|
||||||
# Действия:
|
|
||||||
def get_checkbox_locator(self, text: str) -> Locator:
|
|
||||||
"""Возвращает локатор чек-бокса для элемента списка с указанным текстом.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Текст элемента для выбора.
|
|
||||||
"""
|
|
||||||
|
|
||||||
checkbox_locator = self.page.get_by_role("listitem").filter(has_text=text).get_by_role("checkbox")
|
|
||||||
if checkbox_locator.count() > 1:
|
|
||||||
rtext = f"^{text}$"
|
|
||||||
checkbox_locator = self.page.get_by_role("listitem").filter(
|
|
||||||
has_text=re.compile(rtext)
|
|
||||||
).get_by_role("checkbox")
|
|
||||||
|
|
||||||
expect(checkbox_locator).to_be_visible(), \
|
|
||||||
f"Checkbox for dropdown list item with text {text} is missing"
|
|
||||||
return checkbox_locator
|
|
||||||
|
|
||||||
def deselect_item_with_text(self, text: str) -> None:
|
|
||||||
"""Выбирает элемент списка по указанному тексту.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Текст элемента для выбора.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.get_checkbox_locator(text).uncheck(force=True)
|
|
||||||
|
|
||||||
def select_item_with_text(self, text: str) -> None:
|
|
||||||
"""Выбирает элемент списка по указанному тексту.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Текст элемента для выбора.
|
|
||||||
"""
|
|
||||||
self.get_checkbox_locator(text).check(force=True)
|
|
||||||
|
|
||||||
def get_selected_items(self, locator: str|Locator) -> list[str]:
|
|
||||||
"""Возвращает список отмеченных элементов."""
|
|
||||||
|
|
||||||
selected_items = []
|
|
||||||
|
|
||||||
list_locator = self.get_locator(locator)
|
|
||||||
|
|
||||||
items = list_locator.get_by_role("listitem").all()
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
if item.get_by_role("checkbox").is_checked():
|
|
||||||
item_text = item.text_content().strip()
|
|
||||||
if item_text:
|
|
||||||
selected_items.append(item_text)
|
|
||||||
|
|
||||||
return selected_items
|
|
||||||
|
|
||||||
# Проверки:
|
|
||||||
|
|
@ -50,7 +50,6 @@ class EditUserModalWindow(ModalWindowComponent):
|
||||||
# Добавление полей формы
|
# Добавление полей формы
|
||||||
elements_locators = self.get_input_fields_locators(
|
elements_locators = self.get_input_fields_locators(
|
||||||
self.page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA))
|
self.page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA))
|
||||||
|
|
||||||
# Поле Имя
|
# Поле Имя
|
||||||
loc = elements_locators.get("Имя").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_NAME)
|
loc = elements_locators.get("Имя").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_NAME)
|
||||||
name_input = TextInput(page, loc, "name_input")
|
name_input = TextInput(page, loc, "name_input")
|
||||||
|
|
@ -213,16 +212,16 @@ class EditUserModalWindow(ModalWindowComponent):
|
||||||
if "blocking_checked" in fields:
|
if "blocking_checked" in fields:
|
||||||
checkbox = self.get_content_item("blocking_checkbox")
|
checkbox = self.get_content_item("blocking_checkbox")
|
||||||
if user_data["blocking_checked"]:
|
if user_data["blocking_checked"]:
|
||||||
checkbox.check()
|
checkbox.check(force=True)
|
||||||
else:
|
else:
|
||||||
checkbox.uncheck()
|
checkbox.uncheck(force=True)
|
||||||
|
|
||||||
if "push_notification_checked" in fields:
|
if "push_notification_checked" in fields:
|
||||||
checkbox = self.get_content_item("push_notification_checkbox")
|
checkbox = self.get_content_item("push_notification_checkbox")
|
||||||
if user_data["push_notification_checked"]:
|
if user_data["push_notification_checked"]:
|
||||||
checkbox.check()
|
checkbox.check(force=True)
|
||||||
else:
|
else:
|
||||||
checkbox.uncheck()
|
checkbox.uncheck(force=True)
|
||||||
|
|
||||||
save_button = self.get_button_by_name("save")
|
save_button = self.get_button_by_name("save")
|
||||||
save_button.click()
|
save_button.click()
|
||||||
|
|
|
||||||
|
|
@ -106,3 +106,16 @@ class SendTestEmailModalWindow(ModalWindowComponent):
|
||||||
|
|
||||||
self.alert.check_alert_presence('\nТестовое сообщение\nотправлено\n')
|
self.alert.check_alert_presence('\nТестовое сообщение\nотправлено\n')
|
||||||
self.alert.check_alert_absence('\nТестовое сообщение\nотправлено\n')
|
self.alert.check_alert_absence('\nТестовое сообщение\nотправлено\n')
|
||||||
|
|
||||||
|
def should_be_error_alert(self, alert_text: str) -> None:
|
||||||
|
"""Проверяет наличие сообщения об неуспешной отправке тестового сообщения.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: Если тулбар отсутствует.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alert_type = self.alert.get_alert_type()
|
||||||
|
assert alert_type == "error", f"Expected error alert, but got {alert_type} alert"
|
||||||
|
|
||||||
|
self.alert.check_alert_presence(alert_text)
|
||||||
|
self.alert.check_alert_absence(alert_text)
|
||||||
|
|
|
||||||
|
|
@ -74,3 +74,9 @@ class ViewTaskModalWindow(ModalWindowComponent):
|
||||||
""" Проверка соответствия заголовка таблицы ожидаемому"""
|
""" Проверка соответствия заголовка таблицы ожидаемому"""
|
||||||
|
|
||||||
self.task_stages_table.check_table_headers(actual_headers, expected_headers)
|
self.task_stages_table.check_table_headers(actual_headers, expected_headers)
|
||||||
|
|
||||||
|
def check_stages_table_row_highlighting(self, row_index: int) -> None:
|
||||||
|
"""Проверяет выделение указанной строки таблицы.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.task_stages_table.check_row_highlighting(self.task_stages_table_locator, row_index)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
"""Модуль контейнера для пересоздания сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Содержит класс для работы с формой для пересоздания
|
||||||
|
сертификата во вкладке 'Сертификаты' через Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from playwright.sync_api import Page
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from locators.certificate_locators import CertificateLocators
|
||||||
|
from elements.text_input_element import TextInput
|
||||||
|
from elements.text_element import Text
|
||||||
|
from elements.tooltip_button_element import TooltipButton
|
||||||
|
from components.toolbar_custom_component import CustomToolbar
|
||||||
|
from components.base_component import BaseComponent
|
||||||
|
|
||||||
|
logger = get_logger("REISSUE_CRTIFICATE_FORM")
|
||||||
|
|
||||||
|
|
||||||
|
class ReissueCertificateForm(BaseComponent):
|
||||||
|
"""Компонент формы для пересоздания сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Предоставляет методы для взаимодействия с элементами
|
||||||
|
формы для пересоздания сертификата во вкладке 'Сертификаты'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, page: Page):
|
||||||
|
"""Инициализирует компонент формы для пересоздания сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Экземпляр страницы Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(page)
|
||||||
|
|
||||||
|
button_locator = page.locator(CertificateLocators.FORM_CONTAINER).get_by_role("button")
|
||||||
|
self.button_reissue = TooltipButton(page, button_locator, "button_reissue")
|
||||||
|
|
||||||
|
self.toolbar_info = CustomToolbar(page)
|
||||||
|
|
||||||
|
# поля блока 'Идентификация CA'
|
||||||
|
identification_title_locator = page.locator(CertificateLocators.BLOCK_HEADER_TEXT). \
|
||||||
|
filter(has_text='Идентификация CA')
|
||||||
|
self.identification_title = Text(page, identification_title_locator, "identification_title")
|
||||||
|
self.identification_cert_name = TextInput(page, CertificateLocators.FIELD_INPUT_CERT_NAME,
|
||||||
|
"identification_cert_name_field")
|
||||||
|
self.identification_organization = TextInput(page, CertificateLocators.FIELD_INPUT_ORGANIZATION,
|
||||||
|
"identification_organization_field")
|
||||||
|
self.identification_org_unit = TextInput(page, CertificateLocators.FIELD_INPUT_ORG_UNIT,
|
||||||
|
"identification_org_unit_field")
|
||||||
|
|
||||||
|
# поля блока 'Адрес / Местонахождение'
|
||||||
|
location_title_locator = page.locator(CertificateLocators.BLOCK_HEADER_TEXT). \
|
||||||
|
filter(has_text='Адрес / Местонахождение')
|
||||||
|
self.location_title = Text(page, location_title_locator, "location_title")
|
||||||
|
self.location_country = TextInput(page, CertificateLocators.FIELD_INPUT_COUNTRY, "location_country_field")
|
||||||
|
self.location_state = TextInput(page, CertificateLocators.FIELD_INPUT_STATE, "location_state_field")
|
||||||
|
self.location_city = TextInput(page, CertificateLocators.FIELD_INPUT_LOC, "location_city_field")
|
||||||
|
|
||||||
|
# Действия:
|
||||||
|
def get_identification_fields_values(self) -> dict:
|
||||||
|
"""Возвращает текущее значение полей блока 'Идентификация CA'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict : Текущее значение полей блока 'Идентификация CA'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
values.update({"CN": self.identification_cert_name.get_input_value().strip()})
|
||||||
|
values.update({"O": self.identification_organization.get_input_value().strip()})
|
||||||
|
values.update({"OU": self.identification_org_unit.get_input_value().strip()})
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def get_location_fields_values(self) -> dict:
|
||||||
|
"""Возвращает текущее значение полей блока 'Адрес / Местонахождение'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict : Текущее значение полей блока блока 'Адрес / Местонахождение'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
values.update({"C": self.location_country.get_input_value().strip()})
|
||||||
|
values.update({"ST": self.location_state.get_input_value().strip()})
|
||||||
|
values.update({"L": self.location_city.get_input_value().strip()})
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def input_identification_cert_name_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Имя Сертификата' блока 'Идентификация CA'"""
|
||||||
|
|
||||||
|
self.identification_cert_name.clear()
|
||||||
|
self.identification_cert_name.input_value(value)
|
||||||
|
|
||||||
|
def input_identification_organization_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Организация' блока 'Идентификация CA'"""
|
||||||
|
|
||||||
|
self.identification_organization.clear()
|
||||||
|
self.identification_organization.input_value(value)
|
||||||
|
|
||||||
|
def input_identification_org_unit_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Подразделение' блока 'Идентификация CA'"""
|
||||||
|
|
||||||
|
self.identification_org_unit.clear()
|
||||||
|
self.identification_org_unit.input_value(value)
|
||||||
|
|
||||||
|
def input_location_country_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Страна' блока 'Адрес / Местонахождение'"""
|
||||||
|
|
||||||
|
self.location_country.clear()
|
||||||
|
self.location_country.input_value(value)
|
||||||
|
|
||||||
|
def input_location_state_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Регион / Область' блока 'Адрес / Местонахождение'"""
|
||||||
|
|
||||||
|
self.location_state.clear()
|
||||||
|
self.location_state.input_value(value)
|
||||||
|
|
||||||
|
def input_location_city_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Город' блока 'Адрес / Местонахождение'"""
|
||||||
|
|
||||||
|
self.location_city.clear()
|
||||||
|
self.location_city.input_value(value)
|
||||||
|
|
||||||
|
def _get_label_for_input_field(self, field_locator: str) -> str:
|
||||||
|
div_loc = f"//div[contains(@class, 'flex')][.{field_locator}]"
|
||||||
|
label = self.page.locator(div_loc).locator("//preceding-sibling::div[1]").locator("//input")
|
||||||
|
return label.input_value()
|
||||||
|
|
||||||
|
# Проверки:
|
||||||
|
def check_content(self):
|
||||||
|
"""Проверяет наличие и корректность всех элементов формы."""
|
||||||
|
|
||||||
|
self.button_reissue.check_visibility("Reissue certificate button is missing")
|
||||||
|
assert self.button_reissue.is_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
self.button_reissue.check_tooltip_with_text("Пересоздание сертификата (CA)")
|
||||||
|
|
||||||
|
# Проверка информационного тулбара
|
||||||
|
self.toolbar_info.check_toolbar_presence(['Создание нового сертификата',
|
||||||
|
'Приведет к замене корневого сертификата системы'])
|
||||||
|
# проверка наличия всех полей формы
|
||||||
|
self.identification_title.check_visibility("Title 'Идентификация CA' is missing")
|
||||||
|
|
||||||
|
cert_name_label = self._get_label_for_input_field(CertificateLocators.FIELD_INPUT_CERT_NAME).strip()
|
||||||
|
assert cert_name_label == 'ИМЯ СЕРТИФИКАТА (CN)', f"Unexpected field name {cert_name_label} has got"
|
||||||
|
self.identification_cert_name.check_visibility("Field certificate name input is missing")
|
||||||
|
|
||||||
|
organization_label = self._get_label_for_input_field(CertificateLocators.FIELD_INPUT_ORGANIZATION).strip()
|
||||||
|
assert organization_label == 'ОРГАНИЗАЦИЯ (О)', f"Unexpected field name {organization_label} has got"
|
||||||
|
self.identification_organization.check_visibility("Field organization input is missing")
|
||||||
|
|
||||||
|
org_unit_label = self._get_label_for_input_field(CertificateLocators.FIELD_INPUT_ORG_UNIT).strip()
|
||||||
|
assert org_unit_label == 'ПОДРАЗДЕЛЕНИЕ (OU)', f"Unexpected field name {org_unit_label} has got"
|
||||||
|
self.identification_org_unit.check_visibility("Field organization unit input is missing")
|
||||||
|
|
||||||
|
self.location_title.check_visibility("Title 'Адрес / Местонахождение' is missing")
|
||||||
|
|
||||||
|
country_label = self._get_label_for_input_field(CertificateLocators.FIELD_INPUT_COUNTRY).strip()
|
||||||
|
assert country_label == 'СТРАНА (С)', f"Unexpected field name {country_label} has got"
|
||||||
|
self.location_country.check_visibility("Field country input is missing")
|
||||||
|
|
||||||
|
state_label = self._get_label_for_input_field(CertificateLocators.FIELD_INPUT_STATE).strip()
|
||||||
|
assert state_label == 'РЕГИОН / ОБЛАСТЬ (ST)', f"Unexpected field name {state_label} has got"
|
||||||
|
self.location_state.check_visibility("Field state input is missing")
|
||||||
|
|
||||||
|
city_label = self._get_label_for_input_field(CertificateLocators.FIELD_INPUT_LOC).strip()
|
||||||
|
assert city_label == 'ГОРОД (l)', f"Unexpected field name {city_label} has got"
|
||||||
|
self.location_city.check_visibility("Field city input is missing")
|
||||||
|
|
||||||
|
def is_reissue_button_disabled(self) -> bool:
|
||||||
|
"""Проверяет доступность кнопки перевыпуска сертификата."""
|
||||||
|
|
||||||
|
return self.button_reissue.is_disabled()
|
||||||
|
|
@ -55,6 +55,10 @@ class SelectionBarComponent(BaseComponent):
|
||||||
clear_button_locator = self.selection_bar_locator.locator(
|
clear_button_locator = self.selection_bar_locator.locator(
|
||||||
SelectionBarLocators.CLEAR_SELECTION_BUTTON
|
SelectionBarLocators.CLEAR_SELECTION_BUTTON
|
||||||
)
|
)
|
||||||
|
if clear_button_locator.count() == 0:
|
||||||
|
clear_button_locator = self.selection_bar_locator.locator("../..").locator(
|
||||||
|
SelectionBarLocators.CLEAR_SELECTION_BUTTON
|
||||||
|
)
|
||||||
clear_button_locator.click()
|
clear_button_locator.click()
|
||||||
|
|
||||||
def get_available_options(self) -> list[str]:
|
def get_available_options(self) -> list[str]:
|
||||||
|
|
@ -87,8 +91,13 @@ class SelectionBarComponent(BaseComponent):
|
||||||
def get_selection_bar_title(self) -> str:
|
def get_selection_bar_title(self) -> str:
|
||||||
"""Возвращает название панели выбора значения"""
|
"""Возвращает название панели выбора значения"""
|
||||||
|
|
||||||
|
title_text = ""
|
||||||
title_locator = self.selection_bar_locator.locator(SelectionBarLocators.TITLE_LOCATOR)
|
title_locator = self.selection_bar_locator.locator(SelectionBarLocators.TITLE_LOCATOR)
|
||||||
return title_locator.text_content()
|
if title_locator.count() > 0:
|
||||||
|
title_text = title_locator.text_content()
|
||||||
|
else:
|
||||||
|
title_text = self.selection_bar_locator.get_attribute("placeholder")
|
||||||
|
return title_text
|
||||||
|
|
||||||
def get_selected_values(self) -> list[str]:
|
def get_selected_values(self) -> list[str]:
|
||||||
"""Возвращает список выбранных значений"""
|
"""Возвращает список выбранных значений"""
|
||||||
|
|
@ -96,6 +105,11 @@ class SelectionBarComponent(BaseComponent):
|
||||||
selected_values_locator = self.selection_bar_locator.locator(
|
selected_values_locator = self.selection_bar_locator.locator(
|
||||||
SelectionBarLocators.PARAMETERS_SELECTED
|
SelectionBarLocators.PARAMETERS_SELECTED
|
||||||
)
|
)
|
||||||
|
if selected_values_locator.count() == 0:
|
||||||
|
selected_values_locator = self.selection_bar_locator.locator("../..").locator(
|
||||||
|
SelectionBarLocators.PARAMETERS_SELECTED
|
||||||
|
)
|
||||||
|
print(selected_values_locator)
|
||||||
selected_values = selected_values_locator.all_inner_texts()
|
selected_values = selected_values_locator.all_inner_texts()
|
||||||
return selected_values[0].splitlines()
|
return selected_values[0].splitlines()
|
||||||
|
|
||||||
|
|
@ -153,7 +167,16 @@ class SelectionBarComponent(BaseComponent):
|
||||||
self.selection_bar_locator.click(force=True)
|
self.selection_bar_locator.click(force=True)
|
||||||
|
|
||||||
# Ждем появления выпадающего списка
|
# Ждем появления выпадающего списка
|
||||||
self.wait_for_timeout(1500)
|
if self.page.locator(SelectionBarLocators.LIST_ACTIVE).count() == 0:
|
||||||
|
assert False, "Values list is empty"
|
||||||
|
|
||||||
|
if self.page.locator(SelectionBarLocators.LIST_ACTIVE).count() > 1:
|
||||||
|
self.page.locator(SelectionBarLocators.LIST_ACTIVE).last.wait_for(state="attached")
|
||||||
|
else:
|
||||||
|
self.page.locator(SelectionBarLocators.LIST_ACTIVE).wait_for(state="attached")
|
||||||
|
|
||||||
|
#self.page.locator(SelectionBarLocators.LIST_ACTIVE).wait_for(state="attached")
|
||||||
|
# self.wait_for_timeout(1500)
|
||||||
|
|
||||||
def select_value(self, name: str) -> None:
|
def select_value(self, name: str) -> None:
|
||||||
"""Выбор значения из списка"""
|
"""Выбор значения из списка"""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
"""Модуль контейнера для отображения сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Содержит класс для работы с формой для отображения данных
|
||||||
|
сертификата во вкладке 'Сертификаты' через Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
from playwright.sync_api import Page
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from locators.certificate_locators import CertificateLocators
|
||||||
|
from elements.text_input_element import TextInput
|
||||||
|
from elements.text_element import Text
|
||||||
|
from elements.tooltip_button_element import TooltipButton
|
||||||
|
from components.base_component import BaseComponent
|
||||||
|
|
||||||
|
logger = get_logger("VIEW_CRTIFICATE_FORM")
|
||||||
|
|
||||||
|
|
||||||
|
class ViewCertificateForm(BaseComponent):
|
||||||
|
"""Компонент формы для отображения данных сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Предоставляет методы для взаимодействия с элементами
|
||||||
|
формы для отображения данных сертификата во вкладке 'Сертификаты'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, page: Page):
|
||||||
|
"""Инициализирует компонент формы для отображения данных сертификата во вкладке 'Сертификаты'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Экземпляр страницы Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(page)
|
||||||
|
|
||||||
|
button_locator = page.locator(CertificateLocators.FORM_CONTAINER).get_by_role("button")
|
||||||
|
self.button_export = TooltipButton(page, button_locator, "button_export")
|
||||||
|
|
||||||
|
# поля блока 'Основная информация'
|
||||||
|
base_info_title_locator = page.locator(CertificateLocators.BLOCK_HEADER_TEXT). \
|
||||||
|
filter(has_text='Основная информация')
|
||||||
|
self.base_info_title = Text(page, base_info_title_locator, "base_info_title")
|
||||||
|
|
||||||
|
self.base_info_version = TextInput(page, CertificateLocators.FIELD_VERSION, "base_info_version_field")
|
||||||
|
self.base_info_serial_number = TextInput(page, CertificateLocators.FIELD_SERIAL_NUMBER,
|
||||||
|
"base_info_serial_number_field")
|
||||||
|
self.base_info_signature_algorithm = TextInput(page, CertificateLocators.FIELD_SIGNATURE_ALGORITHM,
|
||||||
|
"base_info_signature_algorithm_field")
|
||||||
|
|
||||||
|
# поля блока 'Срок действия'
|
||||||
|
validity_title_locator = page.locator(CertificateLocators.BLOCK_HEADER_TEXT). \
|
||||||
|
filter(has_text='Срок действия')
|
||||||
|
self.validity_title = Text(page, validity_title_locator, "validity_title")
|
||||||
|
self.validity = TextInput(page, CertificateLocators.FIELD_VALIDITY, "validity_validity_field")
|
||||||
|
self.validity_not_before = TextInput(page, CertificateLocators.FIELD_NOT_BEFORE, "validity_not_before_field")
|
||||||
|
self.validity_not_after = TextInput(page, CertificateLocators.FIELD_NOT_AFTER, "validity_not_after_field")
|
||||||
|
|
||||||
|
# поля блока 'Издатель / Субъект'
|
||||||
|
subject_title_locator = page.locator(CertificateLocators.BLOCK_HEADER_TEXT). \
|
||||||
|
filter(has_text='Издатель / Субъект')
|
||||||
|
self.subject_title = Text(page, subject_title_locator, "subject_title")
|
||||||
|
self.subject_cert_name = TextInput(page, CertificateLocators.FIELD_CERT_NAME, "subject_cert_name_field")
|
||||||
|
self.subject_organization = TextInput(page, CertificateLocators.FIELD_ORGANIZATION,
|
||||||
|
"subject_organization_field")
|
||||||
|
self.subject_org_unit = TextInput(page, CertificateLocators.FIELD_ORG_UNIT, "subject_org_unit_field")
|
||||||
|
self.subject_country = TextInput(page, CertificateLocators.FIELD_COUNTRY, "subject_country_field")
|
||||||
|
self.subject_state = TextInput(page, CertificateLocators.FIELD_STATE, "subject_state_field")
|
||||||
|
self.subject_location = TextInput(page, CertificateLocators.FIELD_LOC, "subject_location_field")
|
||||||
|
|
||||||
|
# поля блока 'Ключ и отпечаток'
|
||||||
|
fingerprint_title_locator = page.locator(CertificateLocators.BLOCK_HEADER_TEXT). \
|
||||||
|
filter(has_text='Ключ и отпечаток')
|
||||||
|
self.fingerprint_title = Text(page, fingerprint_title_locator, "fingerprint_title")
|
||||||
|
self.fingerprint_public_key = TextInput(page, CertificateLocators.FIELD_PUBLIC_KEY_FINGERPRINT,
|
||||||
|
"fingerprint_public_key_field")
|
||||||
|
self.fingerprint_algorithm = TextInput(page, CertificateLocators.FIELD_ALGORITHM,
|
||||||
|
"fingerprint_algorithm_field")
|
||||||
|
self.fingerprint_key_size = TextInput(page, CertificateLocators.FIELD_KEY_SIZE,
|
||||||
|
"fingerprint_key_size_field")
|
||||||
|
|
||||||
|
# Действия:
|
||||||
|
def get_certificate(self) -> dict:
|
||||||
|
""" Возвращает значания полей отображаемого сертификата"""
|
||||||
|
|
||||||
|
certificate = {}
|
||||||
|
|
||||||
|
base_info_dict = {}
|
||||||
|
val = self.base_info_version.get_input_value().strip()
|
||||||
|
base_info_dict.update({"version": val})
|
||||||
|
val = self.base_info_serial_number.get_input_value().strip()
|
||||||
|
base_info_dict.update({"serialNumber": val})
|
||||||
|
val = self.base_info_signature_algorithm.get_input_value().strip()
|
||||||
|
base_info_dict.update({"signatureAlgorithm": val})
|
||||||
|
|
||||||
|
validity_dict = {}
|
||||||
|
val = self.validity.get_input_value().strip()
|
||||||
|
validity_dict.update({"status": val})
|
||||||
|
val = self.validity_not_before.get_input_value().strip()
|
||||||
|
validity_dict.update({"notBefore": val})
|
||||||
|
val = self.validity_not_after.get_input_value().strip()
|
||||||
|
validity_dict.update({"notAfter": val})
|
||||||
|
|
||||||
|
fingerprint_dict = {}
|
||||||
|
val = self.fingerprint_public_key.get_input_value().strip()
|
||||||
|
fingerprint_dict.update({"publicKeyFingerprint": val})
|
||||||
|
val = self.fingerprint_algorithm.get_input_value().strip()
|
||||||
|
fingerprint_dict.update({"algorithm": val})
|
||||||
|
val = self.fingerprint_key_size.get_input_value().strip()
|
||||||
|
fingerprint_dict.update({"keySize": int(val)})
|
||||||
|
|
||||||
|
subject_dict = {}
|
||||||
|
if self.subject_country.get_locator().count() != 0:
|
||||||
|
val = self.subject_country.get_input_value().strip()
|
||||||
|
subject_dict.update({"C": val})
|
||||||
|
if self.subject_state.get_locator().count() != 0:
|
||||||
|
val = self.subject_state.get_input_value().strip()
|
||||||
|
subject_dict.update({"ST": val})
|
||||||
|
if self.subject_location.get_locator().count() != 0:
|
||||||
|
val = self.subject_location.get_input_value().strip()
|
||||||
|
subject_dict.update({"L": val})
|
||||||
|
if self.subject_organization.get_locator().count() != 0:
|
||||||
|
val = self.subject_organization.get_input_value().strip()
|
||||||
|
subject_dict.update({"O": val})
|
||||||
|
if self.subject_org_unit.get_locator().count() != 0:
|
||||||
|
val = self.subject_org_unit.get_input_value().strip()
|
||||||
|
subject_dict.update({"OU": val})
|
||||||
|
if self.subject_cert_name.get_locator().count() != 0:
|
||||||
|
val = self.subject_cert_name.get_input_value().strip()
|
||||||
|
subject_dict.update({"CN": val})
|
||||||
|
|
||||||
|
certificate["baseInfo"] = base_info_dict
|
||||||
|
certificate["validity"] = validity_dict
|
||||||
|
certificate["fingerprint"] = fingerprint_dict
|
||||||
|
certificate["subject"] = subject_dict
|
||||||
|
|
||||||
|
return certificate
|
||||||
|
|
||||||
|
|
||||||
|
def export_certificate(self) -> str:
|
||||||
|
"""Нажатие кнопки 'Экспорт сертификата (CA)' в форме отображения сертификата и
|
||||||
|
скачивание текущего корневого сертификата.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str : Полный путь к скачанному файлу.
|
||||||
|
"""
|
||||||
|
|
||||||
|
path_to_download = Path.home() / "Downloads"
|
||||||
|
|
||||||
|
self.button_export.check_visibility("Export certificate button is missing")
|
||||||
|
|
||||||
|
with self.page.expect_download() as download_info:
|
||||||
|
self.button_export.click()
|
||||||
|
download = download_info.value
|
||||||
|
|
||||||
|
download_error = download.failure()
|
||||||
|
assert not download_error, f"Download certificate error: {download_error}"
|
||||||
|
|
||||||
|
file_to_download = os.path.join(path_to_download, download.suggested_filename)
|
||||||
|
download.save_as(file_to_download)
|
||||||
|
|
||||||
|
assert os.path.exists(file_to_download), f"The certificate file '{file_to_download}' not found"
|
||||||
|
assert os.path.getsize(file_to_download) > 0, f"The certificate file '{file_to_download}' is empty"
|
||||||
|
|
||||||
|
return file_to_download
|
||||||
|
|
||||||
|
|
||||||
|
def _get_label_for_input_field(self, field_locator: str) -> str:
|
||||||
|
div_loc = f"//div[contains(@class, 'flex')][.{field_locator}]"
|
||||||
|
label = self.page.locator(div_loc).locator("//preceding-sibling::div[1]").locator("//input")
|
||||||
|
return label.input_value()
|
||||||
|
|
||||||
|
# Проверки:
|
||||||
|
def check_content(self):
|
||||||
|
"""Проверяет наличие и корректность всех элементов формы."""
|
||||||
|
|
||||||
|
self.button_export.check_visibility("Export certificate button is missing")
|
||||||
|
self.button_export.check_tooltip_with_text("Экспорт сертификата CA")
|
||||||
|
|
||||||
|
# проверка наличия всех полей формы
|
||||||
|
self.base_info_title.check_visibility("Title 'Основная информация' is missing")
|
||||||
|
|
||||||
|
version_label = self._get_label_for_input_field(CertificateLocators.FIELD_VERSION).strip()
|
||||||
|
assert version_label == 'ВЕРСИЯ (Version)', f"Unexpected field name {version_label} has got"
|
||||||
|
self.base_info_version.check_visibility("Field version value is missing")
|
||||||
|
|
||||||
|
serial_number_label = self._get_label_for_input_field(CertificateLocators.FIELD_SERIAL_NUMBER).strip()
|
||||||
|
assert serial_number_label == 'СЕРИЙНЫЙ НОМЕР (Serial Number)',\
|
||||||
|
f"Unexpected field name {serial_number_label} has got"
|
||||||
|
self.base_info_serial_number.check_visibility("Field serial number value is missing")
|
||||||
|
|
||||||
|
signature_algorithm_label = self._get_label_for_input_field(CertificateLocators.FIELD_SIGNATURE_ALGORITHM). \
|
||||||
|
strip()
|
||||||
|
assert signature_algorithm_label == 'АЛГОРИТМ ПОДПИСИ (Signature Algorithm)',\
|
||||||
|
f"Unexpected field name {signature_algorithm_label} has got"
|
||||||
|
self.base_info_signature_algorithm.check_visibility("Field signature algorithm value is missing")
|
||||||
|
|
||||||
|
self.validity_title.check_visibility("Title 'Срок действия' is missing")
|
||||||
|
|
||||||
|
validity_label = self._get_label_for_input_field(CertificateLocators.FIELD_VALIDITY).strip()
|
||||||
|
assert validity_label == 'СТАТУС (Validity)',\
|
||||||
|
f"Unexpected field name {validity_label} has got"
|
||||||
|
self.validity.check_visibility("Field validity value is missing")
|
||||||
|
|
||||||
|
validity_not_before_label = self._get_label_for_input_field(CertificateLocators.FIELD_NOT_BEFORE).strip()
|
||||||
|
assert validity_not_before_label == 'ДЕЙСТВИТЕЛЕН С (Not Before)',\
|
||||||
|
f"Unexpected field name {validity_not_before_label} has got"
|
||||||
|
self.validity_not_before.check_visibility("Field validity not before value is missing")
|
||||||
|
|
||||||
|
validity_not_after_label = self._get_label_for_input_field(CertificateLocators.FIELD_NOT_AFTER).strip()
|
||||||
|
assert validity_not_after_label == 'ДЕЙСТВИТЕЛЕН ДО (Not After)',\
|
||||||
|
f"Unexpected field name {validity_not_after_label} has got"
|
||||||
|
self.validity_not_after.check_visibility("Field validity not after value is missing")
|
||||||
|
|
||||||
|
self.subject_title.check_visibility("Title 'Издатель / Субъект' is missing")
|
||||||
|
|
||||||
|
if self.page.locator(CertificateLocators.FIELD_CERT_NAME).count() != 0:
|
||||||
|
cert_name_label = self._get_label_for_input_field(CertificateLocators.FIELD_CERT_NAME).strip()
|
||||||
|
assert cert_name_label == 'ИМЯ СЕРТИФИКАТА (CN)',\
|
||||||
|
f"Unexpected field name {cert_name_label} has got"
|
||||||
|
self.subject_cert_name.check_visibility("Field certificate name value is missing")
|
||||||
|
|
||||||
|
if self.page.locator(CertificateLocators.FIELD_ORGANIZATION).count() != 0:
|
||||||
|
organization_label = self._get_label_for_input_field(CertificateLocators.FIELD_ORGANIZATION).strip()
|
||||||
|
assert organization_label == 'ОРГАНИЗАЦИЯ (О)',\
|
||||||
|
f"Unexpected field name {organization_label} has got"
|
||||||
|
self.subject_organization.check_visibility("Field organization value is missing")
|
||||||
|
|
||||||
|
if self.page.locator(CertificateLocators.FIELD_ORG_UNIT).count() != 0:
|
||||||
|
org_unit_label = self._get_label_for_input_field(CertificateLocators.FIELD_ORG_UNIT).strip()
|
||||||
|
assert org_unit_label == 'ПОДРАЗДЕЛЕНИЕ (OU)',\
|
||||||
|
f"Unexpected field name {org_unit_label} has got"
|
||||||
|
self.subject_org_unit.check_visibility("Field organization unit value is missing")
|
||||||
|
|
||||||
|
if self.page.locator(CertificateLocators.FIELD_COUNTRY).count() != 0:
|
||||||
|
country_label = self._get_label_for_input_field(CertificateLocators.FIELD_COUNTRY).strip()
|
||||||
|
assert country_label == 'СТРАНА (С)',\
|
||||||
|
f"Unexpected field name {country_label} has got"
|
||||||
|
self.subject_country.check_visibility("Field country value is missing")
|
||||||
|
|
||||||
|
if self.page.locator(CertificateLocators.FIELD_STATE).count() != 0:
|
||||||
|
state_label = self._get_label_for_input_field(CertificateLocators.FIELD_STATE).strip()
|
||||||
|
assert state_label == 'РЕГИОН / ОБЛАСТЬ (ST)',\
|
||||||
|
f"Unexpected field name {state_label} has got"
|
||||||
|
self.subject_state.check_visibility("Field state value is missing")
|
||||||
|
|
||||||
|
if self.page.locator(CertificateLocators.FIELD_LOC).count() != 0:
|
||||||
|
location_label = self._get_label_for_input_field(CertificateLocators.FIELD_LOC).strip()
|
||||||
|
assert location_label == 'ГОРОД (l)',\
|
||||||
|
f"Unexpected field name {location_label} has got"
|
||||||
|
self.subject_location.check_visibility("Field location value is missing")
|
||||||
|
|
||||||
|
self.fingerprint_title.check_visibility("Title 'Ключ и отпечаток' is missing")
|
||||||
|
|
||||||
|
public_key_label = self._get_label_for_input_field(CertificateLocators.FIELD_PUBLIC_KEY_FINGERPRINT).strip()
|
||||||
|
assert public_key_label == 'ПУБЛИЧНЫЙ ОТПЕЧАТОК (PublicKeyFingerprint)',\
|
||||||
|
f"Unexpected field name {public_key_label} has got"
|
||||||
|
self.fingerprint_public_key.check_visibility("Field public key value is missing")
|
||||||
|
|
||||||
|
algorithm_label = self._get_label_for_input_field(CertificateLocators.FIELD_ALGORITHM).strip()
|
||||||
|
assert algorithm_label == 'АЛГОРИТМ (Algorithm)',\
|
||||||
|
f"Unexpected field name {algorithm_label} has got"
|
||||||
|
self.fingerprint_algorithm.check_visibility("Field algorithm value is missing")
|
||||||
|
|
||||||
|
key_size_label = self._get_label_for_input_field(CertificateLocators.FIELD_KEY_SIZE).strip()
|
||||||
|
assert key_size_label == 'ДЛИНА КЛЮЧА (Key Size)',\
|
||||||
|
f"Unexpected field name {key_size_label} has got"
|
||||||
|
self.fingerprint_key_size.check_visibility("Field key size value is missing")
|
||||||
|
|
@ -20,7 +20,7 @@ class Environment:
|
||||||
DEVELOP: str = 'develop'
|
DEVELOP: str = 'develop'
|
||||||
|
|
||||||
URLS: Dict[str, str] = {
|
URLS: Dict[str, str] = {
|
||||||
TEST: 'https://192.168.2.76/',
|
TEST: 'https://192.168.236.12/',
|
||||||
DEVELOP: 'https://192.168.2.69/'
|
DEVELOP: 'https://192.168.2.69/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,13 @@ class TabButton(BaseElement):
|
||||||
# (Методы действий будут добавлены по мере необходимости)
|
# (Методы действий будут добавлены по мере необходимости)
|
||||||
|
|
||||||
# Проверки:
|
# Проверки:
|
||||||
# (Методы проверок будут добавлены по мере необходимости)
|
def is_active(self) -> bool:
|
||||||
|
""" Проверяет является ли кнопка-tab активной """
|
||||||
|
|
||||||
|
tab_locator = self.get_locator()
|
||||||
|
attributes = tab_locator.get_attribute("class")
|
||||||
|
|
||||||
|
is_active_tab = False
|
||||||
|
if "v-tabs__item--active" in attributes:
|
||||||
|
is_active_tab = True
|
||||||
|
return is_active_tab
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,469 @@
|
||||||
|
"""Базовый модуль для работы с формами стойки."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, List, Dict, Any, Tuple
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from playwright.sync_api import Page
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from elements.text_input_element import TextInput
|
||||||
|
from components.base_component import BaseComponent
|
||||||
|
from components.dropdown_list_component import DropdownList
|
||||||
|
|
||||||
|
logger = get_logger("BASE_RACK_FORM")
|
||||||
|
logger.setLevel("INFO")
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseRackData:
|
||||||
|
"""Базовый класс для хранения данных стойки."""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name: str = ""
|
||||||
|
serial: str = ""
|
||||||
|
inventory: str = ""
|
||||||
|
comment: str = ""
|
||||||
|
|
||||||
|
# Combobox поля
|
||||||
|
cable_entry: str = ""
|
||||||
|
state: str = ""
|
||||||
|
depth: str = ""
|
||||||
|
usize: str = ""
|
||||||
|
|
||||||
|
# Combobox поля (справочники)
|
||||||
|
owner: str = ""
|
||||||
|
service_org: str = ""
|
||||||
|
project: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRackForm(BaseComponent, ABC):
|
||||||
|
"""Базовый компонент для работы с формами стойки."""
|
||||||
|
|
||||||
|
# Маппинг текстовых полей (должен быть переопределен в наследниках)
|
||||||
|
TEXT_FIELDS_MAPPING: Dict[str, Tuple[str, str]] = {}
|
||||||
|
TEXT_FIELDS_LOCATORS: Dict[str, str] = {}
|
||||||
|
|
||||||
|
# Маппинг combobox полей (должен быть переопределен в наследниках)
|
||||||
|
COMBOBOX_FIELDS_MAPPING: Dict[str, Tuple[str, str, str]] = {}
|
||||||
|
COMBOBOX_FIELDS_LOCATORS: Dict[str, str] = {}
|
||||||
|
|
||||||
|
# Дополнительные типы полей (checkbox и т.д.) - опционально
|
||||||
|
CHECKBOX_FIELDS_MAPPING: Dict[str, Tuple[str, str]] = {}
|
||||||
|
CHECKBOX_FIELDS_LOCATORS: Dict[str, str] = {}
|
||||||
|
|
||||||
|
def __init__(self, page: Page, form_container_locator: str) -> None:
|
||||||
|
"""Инициализирует базовый компонент формы стойки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Экземпляр страницы Playwright
|
||||||
|
form_container_locator: Локатор контейнера формы
|
||||||
|
"""
|
||||||
|
super().__init__(page)
|
||||||
|
self.page = page
|
||||||
|
self.form_container_locator = form_container_locator
|
||||||
|
self.content_items: Dict[str, Any] = {}
|
||||||
|
self.available_fields = None
|
||||||
|
|
||||||
|
# Инициализация полей формы
|
||||||
|
self._init_form_fields()
|
||||||
|
|
||||||
|
def _init_form_fields(self) -> None:
|
||||||
|
"""Инициализирует все поля формы."""
|
||||||
|
container_locator = self.page.locator(self.form_container_locator)
|
||||||
|
if container_locator.count() > 0:
|
||||||
|
self.available_fields = self.get_input_fields_locators(container_locator)
|
||||||
|
|
||||||
|
self._init_text_fields()
|
||||||
|
self._init_combobox_fields()
|
||||||
|
self._init_checkbox_fields()
|
||||||
|
|
||||||
|
def _init_text_fields(self) -> None:
|
||||||
|
"""Инициализирует текстовые поля формы."""
|
||||||
|
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
|
||||||
|
locator = self.TEXT_FIELDS_LOCATORS.get(field_label)
|
||||||
|
if not locator:
|
||||||
|
continue
|
||||||
|
self._init_single_text_field(field_label, locator, widget_name)
|
||||||
|
|
||||||
|
def _init_single_text_field(self, field_label: str, locator: str, widget_name: str) -> None:
|
||||||
|
"""Инициализирует одно текстовое поле."""
|
||||||
|
try:
|
||||||
|
element = self.page.locator(locator).first
|
||||||
|
if element.count() > 0 and element.is_visible():
|
||||||
|
field_input = TextInput(self.page, element, widget_name)
|
||||||
|
self.content_items[widget_name] = field_input
|
||||||
|
logger.debug(f"Initialized text field: '{field_label}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error initializing text field '{field_label}': {e}")
|
||||||
|
|
||||||
|
def _init_combobox_fields(self) -> None:
|
||||||
|
"""Инициализирует combobox поля формы."""
|
||||||
|
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
||||||
|
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label)
|
||||||
|
if not locator:
|
||||||
|
continue
|
||||||
|
self._init_single_combobox_field(field_label, locator, input_name, list_name)
|
||||||
|
|
||||||
|
def _init_single_combobox_field(
|
||||||
|
self, field_label: str, locator: str, input_name: str, list_name: str
|
||||||
|
) -> None:
|
||||||
|
"""Инициализирует одно combobox поле."""
|
||||||
|
try:
|
||||||
|
element = self.page.locator(locator).first
|
||||||
|
if element.count() > 0 and element.is_visible():
|
||||||
|
field_input = TextInput(self.page, element, input_name)
|
||||||
|
self.content_items[input_name] = field_input
|
||||||
|
self.content_items[list_name] = DropdownList(self.page)
|
||||||
|
logger.debug(f"Initialized combobox field: '{field_label}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error initializing combobox field '{field_label}': {e}")
|
||||||
|
|
||||||
|
def _init_checkbox_fields(self) -> None:
|
||||||
|
"""Инициализирует checkbox поля формы (опционально)."""
|
||||||
|
if not self.CHECKBOX_FIELDS_MAPPING:
|
||||||
|
return
|
||||||
|
|
||||||
|
for field_label, (attr_name, widget_name) in self.CHECKBOX_FIELDS_MAPPING.items():
|
||||||
|
locator = self.CHECKBOX_FIELDS_LOCATORS.get(field_label)
|
||||||
|
if not locator:
|
||||||
|
continue
|
||||||
|
self._init_single_checkbox_field(field_label, locator, widget_name)
|
||||||
|
|
||||||
|
def _init_single_checkbox_field(self, field_label: str, locator: str, widget_name: str) -> None:
|
||||||
|
"""Инициализирует одно checkbox поле."""
|
||||||
|
try:
|
||||||
|
checkbox_input = self.page.locator(locator).first
|
||||||
|
if checkbox_input.count() == 0:
|
||||||
|
logger.debug(f"Checkbox '{field_label}' not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Импортируем здесь чтобы избежать циклических импортов
|
||||||
|
from elements.checkbox_element import Checkbox
|
||||||
|
|
||||||
|
checkbox = Checkbox(self.page, checkbox_input, widget_name)
|
||||||
|
self.content_items[widget_name] = checkbox
|
||||||
|
logger.debug(f"Initialized checkbox field: '{field_label}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error initializing checkbox '{field_label}': {e}")
|
||||||
|
|
||||||
|
def get_content_item(self, item_name: str) -> Any:
|
||||||
|
"""Возвращает элемент контента по имени."""
|
||||||
|
return self.content_items.get(item_name)
|
||||||
|
|
||||||
|
def clear_field(self, field_name: str) -> None:
|
||||||
|
"""Очищает указанное поле."""
|
||||||
|
logger.debug(f"Clearing field: '{field_name}'")
|
||||||
|
|
||||||
|
# Проверяем, не является ли поле чекбоксом
|
||||||
|
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||||||
|
logger.debug(f"Field '{field_name}' is a checkbox, skipping clear operation")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Получаем локатор поля
|
||||||
|
locator = self._get_field_locator(field_name)
|
||||||
|
if not locator:
|
||||||
|
logger.warning(f"Unknown field: {field_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
field_element = self.page.locator(locator).first
|
||||||
|
if field_element.count() == 0:
|
||||||
|
logger.debug(f"Field '{field_name}' not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Очистка в зависимости от типа поля
|
||||||
|
if field_name in self.TEXT_FIELDS_LOCATORS:
|
||||||
|
self._clear_text_field(field_element, field_name)
|
||||||
|
elif field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
||||||
|
self._clear_combobox_field(field_element, field_name)
|
||||||
|
|
||||||
|
def _get_field_locator(self, field_name: str) -> Optional[str]:
|
||||||
|
"""Получает локатор поля по его названию."""
|
||||||
|
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
||||||
|
return self.COMBOBOX_FIELDS_LOCATORS[field_name]
|
||||||
|
elif field_name in self.TEXT_FIELDS_LOCATORS:
|
||||||
|
return self.TEXT_FIELDS_LOCATORS[field_name]
|
||||||
|
elif field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||||||
|
return self.CHECKBOX_FIELDS_LOCATORS[field_name]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _clear_text_field(self, field_element, field_name: str) -> None:
|
||||||
|
"""Очищает текстовое поле."""
|
||||||
|
try:
|
||||||
|
field_element.click()
|
||||||
|
field_element.page.keyboard.press("Control+A")
|
||||||
|
field_element.page.keyboard.press("Backspace")
|
||||||
|
self.wait_for_timeout(200)
|
||||||
|
logger.debug(f"Text field '{field_name}' cleared")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Could not clear text field '{field_name}': {e}")
|
||||||
|
|
||||||
|
def _clear_combobox_field(self, field_element, field_name: str) -> None:
|
||||||
|
"""Очищает combobox поле."""
|
||||||
|
try:
|
||||||
|
parent_container = field_element.locator(
|
||||||
|
"xpath=ancestor::div[contains(@class, 'v-input')]"
|
||||||
|
).first
|
||||||
|
|
||||||
|
if parent_container.count() == 0:
|
||||||
|
logger.debug(f"Parent container not found for field '{field_name}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
clear_button = parent_container.locator(
|
||||||
|
".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle, i.mdi-close"
|
||||||
|
).first
|
||||||
|
|
||||||
|
if clear_button.count() > 0 and clear_button.is_visible():
|
||||||
|
clear_button.click()
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
logger.debug(f"Combobox field '{field_name}' cleared")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Clear button not found for field '{field_name}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error clearing combobox field '{field_name}': {e}")
|
||||||
|
|
||||||
|
def _scroll_to_element_in_dropdown(self, value: str) -> bool:
|
||||||
|
"""Скроллит выпадающий список до элемента с нужным текстом."""
|
||||||
|
logger.debug(f"Scrolling to find element with text: '{value}'")
|
||||||
|
|
||||||
|
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
||||||
|
if dropdown_menu.count() == 0:
|
||||||
|
logger.error("Active dropdown menu not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
max_attempts = 10
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
while attempts < max_attempts:
|
||||||
|
visible_items = dropdown_menu.locator("a.v-list__tile, div[role='listitem']").all()
|
||||||
|
|
||||||
|
if visible_items:
|
||||||
|
for item in visible_items:
|
||||||
|
item_text = item.text_content() or ""
|
||||||
|
if value in item_text:
|
||||||
|
logger.debug(f"Found element with text '{value}'")
|
||||||
|
item.scroll_into_view_if_needed()
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
return True
|
||||||
|
|
||||||
|
last_item = visible_items[-1]
|
||||||
|
last_item_text = last_item.text_content() or ""
|
||||||
|
logger.debug(f"Scrolling to last visible item: '{last_item_text}'")
|
||||||
|
last_item.scroll_into_view_if_needed()
|
||||||
|
self.wait_for_timeout(500)
|
||||||
|
else:
|
||||||
|
dropdown_menu.evaluate("(el) => el.scrollTop += 200")
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
|
logger.warning(f"Element with text '{value}' not found after {max_attempts} attempts")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _fill_text_fields(self, rack_data: BaseRackData, results: Dict[str, int]) -> None:
|
||||||
|
"""Заполняет текстовые поля."""
|
||||||
|
for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items():
|
||||||
|
value = getattr(rack_data, attr_name, "")
|
||||||
|
if not value or not str(value).strip():
|
||||||
|
continue
|
||||||
|
self._fill_single_text_field(field_label, field_name, value, results)
|
||||||
|
|
||||||
|
def _fill_single_text_field(
|
||||||
|
self, field_label: str, field_name: str, value: str, results: Dict[str, int]
|
||||||
|
) -> None:
|
||||||
|
"""Заполняет одно текстовое поле."""
|
||||||
|
try:
|
||||||
|
input_field = self.get_content_item(field_name)
|
||||||
|
if input_field:
|
||||||
|
input_field.input_value(value)
|
||||||
|
results["text_fields_filled"] += 1
|
||||||
|
logger.debug(f"Field '{field_label}' filled: '{value}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error filling field '{field_label}': {e}")
|
||||||
|
|
||||||
|
def _fill_combobox_fields(self, rack_data: BaseRackData, results: Dict[str, int]) -> None:
|
||||||
|
"""Заполняет combobox поля."""
|
||||||
|
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
||||||
|
value = getattr(rack_data, attr_name, "")
|
||||||
|
if not value or not str(value).strip():
|
||||||
|
continue
|
||||||
|
self._fill_single_combobox_field(field_label, input_name, list_name, value, results)
|
||||||
|
|
||||||
|
def _fill_single_combobox_field(
|
||||||
|
self, field_label: str, input_name: str, list_name: str, value: str, results: Dict[str, int]
|
||||||
|
) -> None:
|
||||||
|
"""Заполняет одно combobox поле."""
|
||||||
|
try:
|
||||||
|
combobox_field = self.get_content_item(input_name)
|
||||||
|
if not combobox_field:
|
||||||
|
logger.warning(f"Field '{field_label}' input not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
combobox_field.click(force=True)
|
||||||
|
self.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
if not self._scroll_to_element_in_dropdown(value):
|
||||||
|
logger.error(f"Could not find element with text '{value}' after scrolling")
|
||||||
|
self.page.mouse.click(10, 10)
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
return
|
||||||
|
|
||||||
|
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
||||||
|
item_locator = self._find_dropdown_item(dropdown_menu, value)
|
||||||
|
|
||||||
|
if item_locator and item_locator.count() > 0:
|
||||||
|
item_locator.scroll_into_view_if_needed()
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
item_locator.click()
|
||||||
|
results["combobox_fields_filled"] += 1
|
||||||
|
logger.debug(f"Field '{field_label}' set: '{value}'")
|
||||||
|
self.wait_for_timeout(500)
|
||||||
|
else:
|
||||||
|
logger.error(f"Item with text '{value}' not found in dropdown for field '{field_label}'")
|
||||||
|
self.page.mouse.click(10, 10)
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error filling combobox '{field_label}': {e}")
|
||||||
|
self.page.mouse.click(10, 10)
|
||||||
|
|
||||||
|
def _find_dropdown_item(self, dropdown_menu, value: str):
|
||||||
|
"""Находит элемент в выпадающем списке."""
|
||||||
|
item_locator = dropdown_menu.locator(f"a.v-list__tile:has-text('{value}')").first
|
||||||
|
if item_locator.count() == 0:
|
||||||
|
item_locator = dropdown_menu.locator(f"span:has-text('{value}')").first
|
||||||
|
if item_locator.count() == 0:
|
||||||
|
item_locator = dropdown_menu.locator(f"div[role='listitem']:has-text('{value}')").first
|
||||||
|
return item_locator
|
||||||
|
|
||||||
|
def _fill_checkbox_fields(self, rack_data: BaseRackData, results: Dict[str, int]) -> None:
|
||||||
|
"""Заполняет checkbox поля (опционально)."""
|
||||||
|
if not hasattr(self, 'CHECKBOX_FIELDS_MAPPING'):
|
||||||
|
return
|
||||||
|
|
||||||
|
for field_label, (attr_name, widget_name) in self.CHECKBOX_FIELDS_MAPPING.items():
|
||||||
|
value = getattr(rack_data, attr_name, None)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
self._fill_single_checkbox_field(field_label, widget_name, value, results)
|
||||||
|
|
||||||
|
def _fill_single_checkbox_field(
|
||||||
|
self, field_label: str, widget_name: str, value: bool, results: Dict[str, int]
|
||||||
|
) -> None:
|
||||||
|
"""Заполняет одно checkbox поле."""
|
||||||
|
try:
|
||||||
|
checkbox = self.get_content_item(widget_name)
|
||||||
|
if not checkbox:
|
||||||
|
logger.warning(f"Checkbox '{field_label}' not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
if value:
|
||||||
|
checkbox.check(force=True)
|
||||||
|
logger.debug(f"Checkbox '{field_label}' checked")
|
||||||
|
else:
|
||||||
|
checkbox.uncheck(force=True)
|
||||||
|
logger.debug(f"Checkbox '{field_label}' unchecked")
|
||||||
|
|
||||||
|
results["checkboxes_set"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting checkbox '{field_label}': {e}")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fill_rack_data(self, rack_data: BaseRackData) -> Dict[str, int]:
|
||||||
|
"""Абстрактный метод для заполнения данных стойки."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
||||||
|
"""Проверяет, подсвечено ли поле как ошибочное."""
|
||||||
|
# Для чекбоксов не проверяем ошибки
|
||||||
|
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||||||
|
return False
|
||||||
|
|
||||||
|
locator = self._get_field_locator(field_name)
|
||||||
|
if not locator:
|
||||||
|
return False
|
||||||
|
|
||||||
|
field_element = self.page.locator(locator).first
|
||||||
|
if field_element.count() == 0:
|
||||||
|
logger.debug(f"Field '{field_name}' not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
parent_input = field_element.locator(
|
||||||
|
"xpath=ancestor::div[contains(@class, 'v-input')]"
|
||||||
|
).first
|
||||||
|
|
||||||
|
if parent_input.count() > 0:
|
||||||
|
class_attr = parent_input.get_attribute("class") or ""
|
||||||
|
is_error = "v-input--error" in class_attr or "error--text" in class_attr
|
||||||
|
logger.debug(f"Field '{field_name}' error state: {is_error}")
|
||||||
|
return is_error
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def verify_required_fields_highlighted(self, field_names: List[str]) -> Dict[str, bool]:
|
||||||
|
"""Проверяет, что указанные поля подсвечены как обязательные."""
|
||||||
|
results = {}
|
||||||
|
for field_name in field_names:
|
||||||
|
results[field_name] = self.is_field_highlighted_as_error(field_name)
|
||||||
|
logger.debug(f"Field '{field_name}' highlighted: {results[field_name]}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
||||||
|
"""Ожидает появления подсветки ошибки на поле."""
|
||||||
|
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||||||
|
return False
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while (time.time() - start_time) * 1000 < timeout:
|
||||||
|
if self.is_field_highlighted_as_error(field_name):
|
||||||
|
return True
|
||||||
|
self.wait_for_timeout(200)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_field_value(self, field_name: str) -> Optional[str]:
|
||||||
|
"""Получает значение поля."""
|
||||||
|
# Для чекбоксов
|
||||||
|
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||||||
|
for field_label, (attr_name, widget_name) in self.CHECKBOX_FIELDS_MAPPING.items():
|
||||||
|
if attr_name == field_name or field_label == field_name:
|
||||||
|
checkbox = self.get_content_item(widget_name)
|
||||||
|
if checkbox:
|
||||||
|
return str(checkbox.is_checked())
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Для текстовых полей
|
||||||
|
if field_name in self.TEXT_FIELDS_LOCATORS:
|
||||||
|
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
|
||||||
|
if attr_name == field_name or field_label == field_name:
|
||||||
|
input_field = self.get_content_item(widget_name)
|
||||||
|
if input_field:
|
||||||
|
return input_field.get_input_value()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Для combobox полей
|
||||||
|
return self._get_combobox_value(field_name)
|
||||||
|
|
||||||
|
def _get_combobox_value(self, field_name: str) -> Optional[str]:
|
||||||
|
"""Получает значение combobox поля."""
|
||||||
|
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_name)
|
||||||
|
if not locator:
|
||||||
|
for field_label, (attr_name, input_name, _) in self.COMBOBOX_FIELDS_MAPPING.items():
|
||||||
|
if attr_name == field_name or field_label == field_name:
|
||||||
|
input_field = self.get_content_item(input_name)
|
||||||
|
if input_field:
|
||||||
|
selections = input_field.element.locator(
|
||||||
|
"xpath=ancestor::div[contains(@class, 'v-select__selections')]"
|
||||||
|
).first
|
||||||
|
if selections.count() > 0:
|
||||||
|
value_span = selections.locator("span").first
|
||||||
|
return value_span.text_content() or ""
|
||||||
|
return None
|
||||||
|
|
||||||
|
element = self.page.locator(locator).first
|
||||||
|
if element.count() > 0:
|
||||||
|
selections = element.locator(
|
||||||
|
"xpath=ancestor::div[contains(@class, 'v-select__selections')]"
|
||||||
|
).first
|
||||||
|
if selections.count() > 0:
|
||||||
|
value_span = selections.locator("span").first
|
||||||
|
return value_span.text_content() or ""
|
||||||
|
return None
|
||||||
|
|
@ -1,43 +1,23 @@
|
||||||
|
# forms/rack_create_form.py
|
||||||
"""Модуль для работы с формой создания стойки."""
|
"""Модуль для работы с формой создания стойки."""
|
||||||
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict, Any
|
from typing import Dict
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page
|
||||||
from tools.logger import get_logger
|
from tools.logger import get_logger
|
||||||
from locators.rack_locators import RackLocators
|
from locators.rack_locators import RackLocators
|
||||||
from elements.text_input_element import TextInput
|
from forms.base_rack_form import BaseRackForm, BaseRackData
|
||||||
from components.base_component import BaseComponent
|
|
||||||
from components.dropdown_list_component import DropdownList
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("CREATE_RACK_FORM")
|
logger = get_logger("CREATE_RACK_FORM")
|
||||||
logger.setLevel("INFO")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CreateRackData:
|
class CreateRackData(BaseRackData):
|
||||||
"""Класс для хранения данных создаваемой стойки."""
|
"""Класс для хранения данных создаваемой стойки."""
|
||||||
|
pass # Используем все поля из базового класса
|
||||||
# Основные поля
|
|
||||||
name: str = ""
|
|
||||||
serial: str = ""
|
|
||||||
inventory: str = ""
|
|
||||||
comment: str = ""
|
|
||||||
|
|
||||||
# Combobox поля
|
|
||||||
cable_entry: str = ""
|
|
||||||
state: str = ""
|
|
||||||
depth: str = ""
|
|
||||||
usize: str = ""
|
|
||||||
|
|
||||||
# Combobox поля (не заполняемые)
|
|
||||||
owner: str = ""
|
|
||||||
service_org: str = ""
|
|
||||||
project: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class CreateRackForm(BaseComponent):
|
class CreateRackForm(BaseRackForm):
|
||||||
"""Компонент для работы с формой создания стойки."""
|
"""Компонент для работы с формой создания стойки."""
|
||||||
|
|
||||||
# Маппинг текстовых полей
|
# Маппинг текстовых полей
|
||||||
|
|
@ -48,7 +28,7 @@ class CreateRackForm(BaseComponent):
|
||||||
"Инвентарный номер": ("inventory", "inventory_input"),
|
"Инвентарный номер": ("inventory", "inventory_input"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Маппинг полей для заполнения combobox полей
|
# Маппинг combobox полей
|
||||||
COMBOBOX_FIELDS_MAPPING = {
|
COMBOBOX_FIELDS_MAPPING = {
|
||||||
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
||||||
"Состояние": ("state", "state_input", "state_list"),
|
"Состояние": ("state", "state_input", "state_list"),
|
||||||
|
|
@ -59,7 +39,7 @@ class CreateRackForm(BaseComponent):
|
||||||
"Проект/Титул": ("project", "project_input", "project_list")
|
"Проект/Титул": ("project", "project_input", "project_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для текстовых полей (из RackLocators)
|
# Локаторы для текстовых полей
|
||||||
TEXT_FIELDS_LOCATORS = {
|
TEXT_FIELDS_LOCATORS = {
|
||||||
"Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME,
|
"Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME,
|
||||||
"Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT,
|
"Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT,
|
||||||
|
|
@ -67,7 +47,7 @@ class CreateRackForm(BaseComponent):
|
||||||
"Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY,
|
"Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для combobox полей (из RackLocators)
|
# Локаторы для combobox полей
|
||||||
COMBOBOX_FIELDS_LOCATORS = {
|
COMBOBOX_FIELDS_LOCATORS = {
|
||||||
"Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE,
|
"Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE,
|
||||||
"Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH,
|
"Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH,
|
||||||
|
|
@ -79,230 +59,11 @@ class CreateRackForm(BaseComponent):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, page: Page) -> None:
|
def __init__(self, page: Page) -> None:
|
||||||
"""Инициализирует компонент формы создания стойки.
|
"""Инициализирует компонент формы создания стойки."""
|
||||||
|
super().__init__(page, RackLocators.CREATE_RACK_FORM_CONTAINER)
|
||||||
Args:
|
|
||||||
page: Экземпляр страницы Playwright
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(page)
|
|
||||||
self.page = page
|
|
||||||
self.content_items = {}
|
|
||||||
self.available_fields = None
|
|
||||||
|
|
||||||
# Инициализация полей формы
|
|
||||||
self._init_form_fields()
|
|
||||||
|
|
||||||
def _init_form_fields(self) -> None:
|
|
||||||
"""Инициализирует все поля формы создания."""
|
|
||||||
|
|
||||||
# Получаем доступные поля формы
|
|
||||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER).nth(1)
|
|
||||||
self.available_fields = self.get_input_fields_locators(container_locator)
|
|
||||||
|
|
||||||
self._init_text_fields()
|
|
||||||
self._init_combobox_fields()
|
|
||||||
|
|
||||||
def _init_text_fields(self) -> None:
|
|
||||||
"""Инициализирует текстовые поля формы."""
|
|
||||||
|
|
||||||
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
|
|
||||||
locator = self.TEXT_FIELDS_LOCATORS.get(field_label)
|
|
||||||
if not locator:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._init_single_text_field(field_label, locator, widget_name)
|
|
||||||
|
|
||||||
def _init_single_text_field(self, field_label: str, locator: str, widget_name: str) -> None:
|
|
||||||
"""Инициализирует одно текстовое поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
locator: Локатор поля
|
|
||||||
widget_name: Имя виджета
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
element = self.page.locator(locator).first
|
|
||||||
if element.count() > 0 and element.is_visible():
|
|
||||||
# Создаем TextInput для поля
|
|
||||||
field_input = TextInput(self.page, element, widget_name)
|
|
||||||
self.content_items[widget_name] = field_input
|
|
||||||
logger.debug(f"Initialized text field: '{field_label}'")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error initializing text field '{field_label}': {e}")
|
|
||||||
|
|
||||||
def _init_combobox_fields(self) -> None:
|
|
||||||
"""Инициализирует combobox поля формы."""
|
|
||||||
|
|
||||||
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
|
||||||
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label)
|
|
||||||
if not locator:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._init_single_combobox_field(field_label, locator, input_name, list_name)
|
|
||||||
|
|
||||||
def _init_single_combobox_field(
|
|
||||||
self, field_label: str, locator: str, input_name: str, list_name: str
|
|
||||||
) -> None:
|
|
||||||
"""Инициализирует одно combobox поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
locator: Локатор поля
|
|
||||||
input_name: Имя поля ввода
|
|
||||||
list_name: Имя списка
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
element = self.page.locator(locator).first
|
|
||||||
if element.count() > 0 and element.is_visible():
|
|
||||||
# Для combobox создаем TextInput для клика
|
|
||||||
field_input = TextInput(self.page, element, input_name)
|
|
||||||
self.content_items[input_name] = field_input
|
|
||||||
# Добавляем DropdownList для выбора значений
|
|
||||||
self.content_items[list_name] = DropdownList(self.page)
|
|
||||||
logger.debug(f"Initialized combobox field: '{field_label}'")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error initializing combobox field '{field_label}': {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def clear_field(self, field_name: str) -> None:
|
|
||||||
"""Очищает указанное поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_name: Название поля для очистки
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.debug(f"Clearing field: '{field_name}'")
|
|
||||||
|
|
||||||
# Получаем локатор поля
|
|
||||||
locator = None
|
|
||||||
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
|
||||||
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
|
|
||||||
elif field_name in self.TEXT_FIELDS_LOCATORS:
|
|
||||||
locator = self.TEXT_FIELDS_LOCATORS[field_name]
|
|
||||||
else:
|
|
||||||
logger.warning(f"Unknown field: {field_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
field_element = self.page.locator(locator).first
|
|
||||||
|
|
||||||
if field_element.count() == 0:
|
|
||||||
logger.debug(f"Field '{field_name}' not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Для текстовых полей
|
|
||||||
if field_name in self.TEXT_FIELDS_LOCATORS:
|
|
||||||
try:
|
|
||||||
field_element.click()
|
|
||||||
field_element.page.keyboard.press("Control+A")
|
|
||||||
field_element.page.keyboard.press("Backspace")
|
|
||||||
self.wait_for_timeout(200)
|
|
||||||
logger.debug(f"Text field '{field_name}' cleared")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Could not clear text field '{field_name}': {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Для combobox полей
|
|
||||||
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
|
||||||
# Поднимаемся до родительского контейнера
|
|
||||||
parent_container = field_element.locator(
|
|
||||||
"xpath=ancestor::div[contains(@class, 'v-input')]"
|
|
||||||
).first
|
|
||||||
|
|
||||||
if parent_container.count() == 0:
|
|
||||||
logger.debug(f"Parent container not found for field '{field_name}'")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ищем кнопку очистки (крестик)
|
|
||||||
clear_button = parent_container.locator(
|
|
||||||
".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle, i.mdi-close"
|
|
||||||
).first
|
|
||||||
|
|
||||||
if clear_button.count() > 0 and clear_button.is_visible():
|
|
||||||
clear_button.click()
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
logger.debug(f"Combobox field '{field_name}' cleared")
|
|
||||||
else:
|
|
||||||
logger.debug(f"Clear button not found for field '{field_name}'")
|
|
||||||
|
|
||||||
def get_content_item(self, item_name: str) -> Any:
|
|
||||||
"""Возвращает элемент контента по имени.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item_name: Имя элемента
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Элемент или None если не найден
|
|
||||||
"""
|
|
||||||
return self.content_items.get(item_name)
|
|
||||||
|
|
||||||
def _scroll_to_element_in_dropdown(self, value: str) -> bool:
|
|
||||||
"""Скроллит выпадающий список до элемента с нужным текстом используя playwright.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: Текст для поиска
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если элемент найден, False в противном случае
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.debug(f"Scrolling to find element with text: '{value}'")
|
|
||||||
|
|
||||||
# Получаем активное выпадающее меню
|
|
||||||
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
|
||||||
|
|
||||||
if dropdown_menu.count() == 0:
|
|
||||||
logger.error("Active dropdown menu not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
max_attempts = 10
|
|
||||||
attempts = 0
|
|
||||||
last_item_text = ""
|
|
||||||
|
|
||||||
while attempts < max_attempts:
|
|
||||||
# Получаем все видимые элементы списка
|
|
||||||
visible_items = dropdown_menu.locator("a.v-list__tile, div[role='listitem']").all()
|
|
||||||
|
|
||||||
if visible_items:
|
|
||||||
# Проверяем каждый видимый элемент
|
|
||||||
for item in visible_items:
|
|
||||||
item_text = item.text_content() or ""
|
|
||||||
if value in item_text:
|
|
||||||
logger.debug(f"Found element with text '{value}'")
|
|
||||||
# Скроллим до элемента
|
|
||||||
item.scroll_into_view_if_needed()
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Если элемент не найден, скроллим до последнего видимого элемента
|
|
||||||
last_item = visible_items[-1]
|
|
||||||
last_item_text = last_item.text_content() or ""
|
|
||||||
logger.debug(f"Scrolling to last visible item: '{last_item_text}'")
|
|
||||||
last_item.scroll_into_view_if_needed()
|
|
||||||
self.wait_for_timeout(500)
|
|
||||||
else:
|
|
||||||
# Если нет видимых элементов, скроллим вниз
|
|
||||||
dropdown_menu.evaluate("(el) => el.scrollTop += 200")
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
|
|
||||||
attempts += 1
|
|
||||||
logger.debug(f"Scroll attempt {attempts}/{max_attempts}")
|
|
||||||
|
|
||||||
logger.warning(f"Element with text '{value}' not found after {max_attempts} scroll attempts")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def fill_rack_data(self, rack_data: CreateRackData) -> Dict[str, int]:
|
def fill_rack_data(self, rack_data: CreateRackData) -> Dict[str, int]:
|
||||||
"""Заполняет поля формы создания стойки.
|
"""Заполняет поля формы создания стойки."""
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Словарь с результатами заполнения
|
|
||||||
"""
|
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
"text_fields_filled": 0,
|
"text_fields_filled": 0,
|
||||||
"combobox_fields_filled": 0,
|
"combobox_fields_filled": 0,
|
||||||
|
|
@ -314,207 +75,3 @@ class CreateRackForm(BaseComponent):
|
||||||
logger.info(f"Filled {results['text_fields_filled']} text fields and "
|
logger.info(f"Filled {results['text_fields_filled']} text fields and "
|
||||||
f"{results['combobox_fields_filled']} combobox fields")
|
f"{results['combobox_fields_filled']} combobox fields")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _fill_text_fields(self, rack_data: CreateRackData, results: Dict[str, int]) -> None:
|
|
||||||
"""Заполняет текстовые поля.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items():
|
|
||||||
value = getattr(rack_data, attr_name, "")
|
|
||||||
if not value or not str(value).strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._fill_single_text_field(field_label, field_name, value, results)
|
|
||||||
|
|
||||||
def _fill_single_text_field(
|
|
||||||
self, field_label: str, field_name: str, value: str, results: Dict[str, int]
|
|
||||||
) -> None:
|
|
||||||
"""Заполняет одно текстовое поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
field_name: Имя поля
|
|
||||||
value: Значение для заполнения
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
input_field = self.get_content_item(field_name)
|
|
||||||
if input_field:
|
|
||||||
input_field.input_value(value)
|
|
||||||
results["text_fields_filled"] += 1
|
|
||||||
logger.debug(f"Field '{field_label}' filled: '{value}'")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error filling field '{field_label}': {e}")
|
|
||||||
|
|
||||||
def _fill_combobox_fields(self, rack_data: CreateRackData, results: Dict[str, int]) -> None:
|
|
||||||
"""Заполняет combobox поля.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
|
||||||
value = getattr(rack_data, attr_name, "")
|
|
||||||
if not value or not str(value).strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._fill_single_combobox_field(
|
|
||||||
field_label, input_name, list_name, value, results
|
|
||||||
)
|
|
||||||
|
|
||||||
def _fill_single_combobox_field(
|
|
||||||
self, field_label: str, input_name: str, list_name: str, value: str, results: Dict[str, int]
|
|
||||||
) -> None:
|
|
||||||
"""Заполняет одно combobox поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
input_name: Имя поля ввода
|
|
||||||
list_name: Имя списка
|
|
||||||
value: Значение для выбора
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
combobox_field = self.get_content_item(input_name)
|
|
||||||
if not combobox_field:
|
|
||||||
logger.warning(f"Field '{field_label}' input not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Кликаем для открытия выпадающего списка
|
|
||||||
combobox_field.click(force=True)
|
|
||||||
self.wait_for_timeout(1000)
|
|
||||||
|
|
||||||
# Скроллим до нужного элемента
|
|
||||||
if not self._scroll_to_element_in_dropdown(value):
|
|
||||||
logger.error(f"Could not find element with text '{value}' after scrolling")
|
|
||||||
# Закрываем выпадающий список кликом вне
|
|
||||||
self.page.mouse.click(10, 10)
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Получаем активное выпадающее меню
|
|
||||||
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
|
||||||
|
|
||||||
# Ищем элемент с нужным текстом
|
|
||||||
item_locator = dropdown_menu.locator(f"a.v-list__tile:has-text('{value}')").first
|
|
||||||
|
|
||||||
if item_locator.count() == 0:
|
|
||||||
item_locator = dropdown_menu.locator(f"span:has-text('{value}')").first
|
|
||||||
|
|
||||||
if item_locator.count() == 0:
|
|
||||||
item_locator = dropdown_menu.locator(f"div[role='listitem']:has-text('{value}')").first
|
|
||||||
|
|
||||||
if item_locator.count() > 0:
|
|
||||||
# Убеждаемся что элемент видим и кликаем
|
|
||||||
item_locator.scroll_into_view_if_needed()
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
item_locator.click()
|
|
||||||
results["combobox_fields_filled"] += 1
|
|
||||||
logger.debug(f"Field '{field_label}' set: '{value}'")
|
|
||||||
|
|
||||||
# Небольшая пауза после выбора
|
|
||||||
self.wait_for_timeout(500)
|
|
||||||
else:
|
|
||||||
logger.error(f"Item with text '{value}' not found in dropdown for field '{field_label}'")
|
|
||||||
# Закрываем выпадающий список кликом вне
|
|
||||||
self.page.mouse.click(10, 10)
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error filling combobox '{field_label}': {e}")
|
|
||||||
self.page.mouse.click(10, 10)
|
|
||||||
|
|
||||||
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
|
||||||
"""Проверяет, подсвечено ли поле как ошибочное.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_name: Название поля для проверки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если поле подсвечено ошибкой, False в противном случае
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Проверяем в текстовых полях
|
|
||||||
if field_name in self.TEXT_FIELDS_LOCATORS:
|
|
||||||
locator = self.TEXT_FIELDS_LOCATORS[field_name]
|
|
||||||
field_element = self.page.locator(locator).first
|
|
||||||
|
|
||||||
if field_element.count() == 0:
|
|
||||||
logger.debug(f"Field '{field_name}' not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Поднимаемся до родительского контейнера с классом v-input
|
|
||||||
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
|
|
||||||
|
|
||||||
if parent_input.count() > 0:
|
|
||||||
# Проверяем наличие класса ошибки
|
|
||||||
class_attr = parent_input.get_attribute("class") or ""
|
|
||||||
is_error = "v-input--error" in class_attr or "error--text" in class_attr
|
|
||||||
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
|
|
||||||
return is_error
|
|
||||||
|
|
||||||
# Проверяем в combobox полях
|
|
||||||
elif field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
|
||||||
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
|
|
||||||
field_element = self.page.locator(locator).first
|
|
||||||
|
|
||||||
if field_element.count() == 0:
|
|
||||||
logger.debug(f"Field '{field_name}' not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Поднимаемся до родительского контейнера с классом v-input
|
|
||||||
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
|
|
||||||
|
|
||||||
if parent_input.count() > 0:
|
|
||||||
# Проверяем наличие класса ошибки
|
|
||||||
class_attr = parent_input.get_attribute("class") or ""
|
|
||||||
is_error = "v-input--error" in class_attr or "error--text" in class_attr
|
|
||||||
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
|
|
||||||
return is_error
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def verify_required_fields_highlighted(self, field_names: List[str]) -> Dict[str, bool]:
|
|
||||||
"""Проверяет, что указанные поля подсвечены как обязательные (с ошибкой).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_names: Список названий полей для проверки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Словарь с результатами проверки {field_name: is_highlighted}
|
|
||||||
"""
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
for field_name in field_names:
|
|
||||||
results[field_name] = self.is_field_highlighted_as_error(field_name)
|
|
||||||
logger.debug(f"Field '{field_name}' highlighted: {results[field_name]}")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
|
||||||
"""Ожидает появления подсветки ошибки на поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_name: Название поля
|
|
||||||
timeout: Таймаут в миллисекундах
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если ошибка появилась, False в противном случае
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
while (time.time() - start_time) * 1000 < timeout:
|
|
||||||
if self.is_field_highlighted_as_error(field_name):
|
|
||||||
return True
|
|
||||||
self.wait_for_timeout(200)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,28 @@
|
||||||
"""Модуль для работы с формой редактирования стойки в модальном окне."""
|
# forms/rack_edit_form.py
|
||||||
|
"""Модуль для работы с формой редактирования стойки."""
|
||||||
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, Dict
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page
|
||||||
from tools.logger import get_logger
|
from tools.logger import get_logger
|
||||||
from locators.rack_locators import RackLocators
|
from locators.rack_locators import RackLocators
|
||||||
from elements.text_input_element import TextInput
|
from forms.base_rack_form import BaseRackForm, BaseRackData
|
||||||
from components.base_component import BaseComponent
|
|
||||||
from components.dropdown_list_component import DropdownList
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("EDIT_RACK_FORM")
|
logger = get_logger("EDIT_RACK_FORM")
|
||||||
logger.setLevel("INFO")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EditRackFormData:
|
class EditRackData(BaseRackData):
|
||||||
"""Класс для хранения данных редактируемой стойки."""
|
"""Класс для хранения данных редактируемой стойки."""
|
||||||
|
|
||||||
# Основные поля
|
# Дополнительное поле для формы редактирования
|
||||||
name: str = ""
|
|
||||||
serial: str = ""
|
|
||||||
inventory: str = ""
|
|
||||||
comment: str = ""
|
|
||||||
allocated_power: str = ""
|
allocated_power: str = ""
|
||||||
|
|
||||||
# Combobox поля
|
|
||||||
cable_entry: str = ""
|
|
||||||
state: str = ""
|
|
||||||
depth: str = ""
|
|
||||||
usize: str = ""
|
|
||||||
|
|
||||||
# Combobox поля (не заполняемые)
|
|
||||||
owner: str = ""
|
|
||||||
service_org: str = ""
|
|
||||||
project: str = ""
|
|
||||||
|
|
||||||
# Checkbox поле
|
|
||||||
ventilation_panel: Optional[bool] = None
|
ventilation_panel: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class EditRackForm(BaseComponent):
|
class EditRackForm(BaseRackForm):
|
||||||
"""Компонент для работы с формой редактирования стойки в модальном окне."""
|
"""Компонент для работы с формой редактирования стойки."""
|
||||||
|
|
||||||
# Маппинг текстовых полей
|
# Маппинг текстовых полей
|
||||||
TEXT_FIELDS_MAPPING = {
|
TEXT_FIELDS_MAPPING = {
|
||||||
|
|
@ -53,7 +33,7 @@ class EditRackForm(BaseComponent):
|
||||||
"Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"),
|
"Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Маппинг полей для заполнения combobox полей
|
# Маппинг combobox полей
|
||||||
COMBOBOX_FIELDS_MAPPING = {
|
COMBOBOX_FIELDS_MAPPING = {
|
||||||
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
||||||
"Состояние": ("state", "state_input", "state_list"),
|
"Состояние": ("state", "state_input", "state_list"),
|
||||||
|
|
@ -64,7 +44,12 @@ class EditRackForm(BaseComponent):
|
||||||
"Проект/Титул": ("project", "project_input", "project_list")
|
"Проект/Титул": ("project", "project_input", "project_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для текстовых полей (из RackLocators)
|
# Маппинг checkbox полей
|
||||||
|
CHECKBOX_FIELDS_MAPPING = {
|
||||||
|
"Вентиляционная панель": ("ventilation_panel", "ventilation_checkbox"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Локаторы для текстовых полей
|
||||||
TEXT_FIELDS_LOCATORS = {
|
TEXT_FIELDS_LOCATORS = {
|
||||||
"Имя": RackLocators.EDIT_RACK_FORM_FIELD_NAME,
|
"Имя": RackLocators.EDIT_RACK_FORM_FIELD_NAME,
|
||||||
"Комментарий": RackLocators.EDIT_RACK_FORM_FIELD_COMMENT,
|
"Комментарий": RackLocators.EDIT_RACK_FORM_FIELD_COMMENT,
|
||||||
|
|
@ -73,7 +58,7 @@ class EditRackForm(BaseComponent):
|
||||||
"Выделенная мощность (Вт/ВА)": RackLocators.EDIT_RACK_FORM_FIELD_POWER,
|
"Выделенная мощность (Вт/ВА)": RackLocators.EDIT_RACK_FORM_FIELD_POWER,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для combobox полей (из RackLocators)
|
# Локаторы для combobox полей
|
||||||
COMBOBOX_FIELDS_LOCATORS = {
|
COMBOBOX_FIELDS_LOCATORS = {
|
||||||
"Ввод кабеля": RackLocators.EDIT_RACK_FORM_SELECT_CABLE_INPUT,
|
"Ввод кабеля": RackLocators.EDIT_RACK_FORM_SELECT_CABLE_INPUT,
|
||||||
"Состояние": RackLocators.EDIT_RACK_FORM_SELECT_CONDITION_TYPE,
|
"Состояние": RackLocators.EDIT_RACK_FORM_SELECT_CONDITION_TYPE,
|
||||||
|
|
@ -84,262 +69,17 @@ class EditRackForm(BaseComponent):
|
||||||
"Проект/Титул": RackLocators.EDIT_RACK_FORM_SELECT_PROJECT,
|
"Проект/Титул": RackLocators.EDIT_RACK_FORM_SELECT_PROJECT,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локатор для чекбокса вентиляционной панели
|
# Локаторы для checkbox полей
|
||||||
CHECKBOX_VENTILATION = RackLocators.EDIT_RACK_FORM_CHECKBOX_VENTILATION
|
CHECKBOX_FIELDS_LOCATORS = {
|
||||||
|
"Вентиляционная панель": RackLocators.EDIT_RACK_FORM_CHECKBOX_VENTILATION,
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, page: Page) -> None:
|
def __init__(self, page: Page) -> None:
|
||||||
"""Инициализирует компонент формы редактирования стойки.
|
"""Инициализирует компонент формы редактирования стойки."""
|
||||||
|
super().__init__(page, RackLocators.EDIT_RACK_FORM)
|
||||||
|
|
||||||
Args:
|
def fill_rack_data(self, rack_data: EditRackData) -> Dict[str, int]:
|
||||||
page: Экземпляр страницы Playwright
|
"""Заполняет поля формы редактирования стойки."""
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(page)
|
|
||||||
self.page = page
|
|
||||||
self.content_items = {}
|
|
||||||
self.available_fields = None
|
|
||||||
|
|
||||||
# Инициализация полей формы
|
|
||||||
self._init_form_fields()
|
|
||||||
|
|
||||||
def _init_form_fields(self) -> None:
|
|
||||||
"""Инициализирует все поля формы редактирования."""
|
|
||||||
|
|
||||||
# Получаем доступные поля формы
|
|
||||||
container_locator = self.page.locator(RackLocators.EDIT_RACK_FORM)
|
|
||||||
self.available_fields = self.get_input_fields_locators(container_locator)
|
|
||||||
|
|
||||||
self._init_text_fields()
|
|
||||||
self._init_combobox_fields()
|
|
||||||
self._init_checkbox_fields()
|
|
||||||
|
|
||||||
def _init_text_fields(self) -> None:
|
|
||||||
"""Инициализирует текстовые поля формы."""
|
|
||||||
|
|
||||||
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
|
|
||||||
locator = self.TEXT_FIELDS_LOCATORS.get(field_label)
|
|
||||||
if not locator:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._init_single_text_field(field_label, locator, widget_name)
|
|
||||||
|
|
||||||
def _init_single_text_field(self, field_label: str, locator: str, widget_name: str) -> None:
|
|
||||||
"""Инициализирует одно текстовое поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
locator: Локатор поля
|
|
||||||
widget_name: Имя виджета
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
element = self.page.locator(locator).first
|
|
||||||
if element.count() > 0 and element.is_visible():
|
|
||||||
# Создаем TextInput для поля
|
|
||||||
field_input = TextInput(self.page, element, widget_name)
|
|
||||||
self.content_items[widget_name] = field_input
|
|
||||||
logger.debug(f"Initialized text field: '{field_label}'")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error initializing text field '{field_label}': {e}")
|
|
||||||
|
|
||||||
def _init_combobox_fields(self) -> None:
|
|
||||||
"""Инициализирует combobox поля формы."""
|
|
||||||
|
|
||||||
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
|
||||||
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label)
|
|
||||||
if not locator:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._init_single_combobox_field(field_label, locator, input_name, list_name)
|
|
||||||
|
|
||||||
def _init_single_combobox_field(
|
|
||||||
self, field_label: str, locator: str, input_name: str, list_name: str
|
|
||||||
) -> None:
|
|
||||||
"""Инициализирует одно combobox поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
locator: Локатор поля
|
|
||||||
input_name: Имя поля ввода
|
|
||||||
list_name: Имя списка
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
element = self.page.locator(locator).first
|
|
||||||
if element.count() > 0 and element.is_visible():
|
|
||||||
# Для combobox создаем TextInput для клика
|
|
||||||
field_input = TextInput(self.page, element, input_name)
|
|
||||||
self.content_items[input_name] = field_input
|
|
||||||
# Добавляем DropdownList для выбора значений
|
|
||||||
self.content_items[list_name] = DropdownList(self.page)
|
|
||||||
logger.debug(f"Initialized combobox field: '{field_label}'")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error initializing combobox field '{field_label}': {e}")
|
|
||||||
|
|
||||||
def _init_checkbox_fields(self) -> None:
|
|
||||||
"""Инициализирует checkbox поля формы."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._init_ventilation_checkbox()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error initializing checkbox: {e}")
|
|
||||||
|
|
||||||
def _init_ventilation_checkbox(self) -> None:
|
|
||||||
"""Инициализирует чекбокс вентиляционной панели."""
|
|
||||||
|
|
||||||
checkbox_input = self.page.locator(self.CHECKBOX_VENTILATION).first
|
|
||||||
|
|
||||||
if checkbox_input.count() == 0:
|
|
||||||
logger.debug("Ventilation panel checkbox not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Импортируем Checkbox только здесь чтобы избежать циклических импортов
|
|
||||||
from elements.checkbox_element import Checkbox
|
|
||||||
|
|
||||||
checkbox = Checkbox(self.page, checkbox_input, "ventilation_panel")
|
|
||||||
self.content_items["ventilation_checkbox"] = checkbox
|
|
||||||
|
|
||||||
logger.debug("Initialized ventilation panel checkbox")
|
|
||||||
|
|
||||||
def clear_field(self, field_name: str) -> None:
|
|
||||||
"""Очищает указанное поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_name: Название поля для очистки
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.debug(f"Clearing field: '{field_name}'")
|
|
||||||
|
|
||||||
# Проверяем, не является ли поле чекбоксом
|
|
||||||
if field_name == "Вентиляционная панель":
|
|
||||||
logger.debug(f"Field '{field_name}' is a checkbox, skipping clear operation")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Получаем локатор поля
|
|
||||||
locator = None
|
|
||||||
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
|
||||||
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
|
|
||||||
elif field_name in self.TEXT_FIELDS_LOCATORS:
|
|
||||||
locator = self.TEXT_FIELDS_LOCATORS[field_name]
|
|
||||||
else:
|
|
||||||
logger.warning(f"Unknown field: {field_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
field_element = self.page.locator(locator).first
|
|
||||||
|
|
||||||
if field_element.count() == 0:
|
|
||||||
logger.debug(f"Field '{field_name}' not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Для текстовых полей
|
|
||||||
if field_name in self.TEXT_FIELDS_LOCATORS:
|
|
||||||
try:
|
|
||||||
field_element.click()
|
|
||||||
field_element.page.keyboard.press("Control+A")
|
|
||||||
field_element.page.keyboard.press("Backspace")
|
|
||||||
self.wait_for_timeout(200)
|
|
||||||
logger.debug(f"Text field '{field_name}' cleared")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Could not clear text field '{field_name}': {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Для combobox полей
|
|
||||||
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
|
||||||
# Поднимаемся до родительского контейнера
|
|
||||||
parent_container = field_element.locator(
|
|
||||||
"xpath=ancestor::div[contains(@class, 'v-input')]"
|
|
||||||
).first
|
|
||||||
|
|
||||||
if parent_container.count() == 0:
|
|
||||||
logger.debug(f"Parent container not found for field '{field_name}'")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ищем кнопку очистки (крестик)
|
|
||||||
clear_button = parent_container.locator(
|
|
||||||
".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle, i.mdi-close"
|
|
||||||
).first
|
|
||||||
|
|
||||||
if clear_button.count() > 0 and clear_button.is_visible():
|
|
||||||
clear_button.click()
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
logger.debug(f"Combobox field '{field_name}' cleared")
|
|
||||||
else:
|
|
||||||
logger.debug(f"Clear button not found for field '{field_name}'")
|
|
||||||
|
|
||||||
def get_content_item(self, item_name: str) -> Any:
|
|
||||||
"""Возвращает элемент контента по имени.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item_name: Имя элемента
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Элемент или None если не найден
|
|
||||||
"""
|
|
||||||
return self.content_items.get(item_name)
|
|
||||||
|
|
||||||
def _scroll_to_element_in_dropdown(self, value: str) -> bool:
|
|
||||||
"""Скроллит выпадающий список до элемента с нужным текстом используя playwright.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: Текст для поиска
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если элемент найден, False в противном случае
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.debug(f"Scrolling to find element with text: '{value}'")
|
|
||||||
|
|
||||||
# Получаем активное выпадающее меню
|
|
||||||
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
|
||||||
|
|
||||||
if dropdown_menu.count() == 0:
|
|
||||||
logger.error("Active dropdown menu not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
max_attempts = 10
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
while attempts < max_attempts:
|
|
||||||
# Получаем все видимые элементы списка
|
|
||||||
visible_items = dropdown_menu.locator("a.v-list__tile, div[role='listitem']").all()
|
|
||||||
|
|
||||||
if visible_items:
|
|
||||||
# Проверяем каждый видимый элемент
|
|
||||||
for item in visible_items:
|
|
||||||
item_text = item.text_content() or ""
|
|
||||||
if value in item_text:
|
|
||||||
logger.debug(f"Found element with text '{value}'")
|
|
||||||
# Скроллим до элемента
|
|
||||||
item.scroll_into_view_if_needed()
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Если элемент не найден, скроллим до последнего видимого элемента
|
|
||||||
last_item = visible_items[-1]
|
|
||||||
last_item_text = last_item.text_content() or ""
|
|
||||||
logger.debug(f"Scrolling to last visible item: '{last_item_text}'")
|
|
||||||
last_item.scroll_into_view_if_needed()
|
|
||||||
self.wait_for_timeout(500)
|
|
||||||
else:
|
|
||||||
# Если нет видимых элементов, скроллим вниз
|
|
||||||
dropdown_menu.evaluate("(el) => el.scrollTop += 200")
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
|
|
||||||
attempts += 1
|
|
||||||
logger.debug(f"Scroll attempt {attempts}/{max_attempts}")
|
|
||||||
|
|
||||||
logger.warning(f"Element with text '{value}' not found after {max_attempts} scroll attempts")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def fill_rack_data(self, rack_data: EditRackFormData) -> Dict[str, int]:
|
|
||||||
"""Заполняет поля формы редактирования стойки.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Словарь с результатами заполнения
|
|
||||||
"""
|
|
||||||
results = {
|
results = {
|
||||||
"text_fields_filled": 0,
|
"text_fields_filled": 0,
|
||||||
"combobox_fields_filled": 0,
|
"combobox_fields_filled": 0,
|
||||||
|
|
@ -348,302 +88,9 @@ class EditRackForm(BaseComponent):
|
||||||
|
|
||||||
self._fill_text_fields(rack_data, results)
|
self._fill_text_fields(rack_data, results)
|
||||||
self._fill_combobox_fields(rack_data, results)
|
self._fill_combobox_fields(rack_data, results)
|
||||||
self._set_checkbox(rack_data, results)
|
self._fill_checkbox_fields(rack_data, results)
|
||||||
|
|
||||||
logger.info(f"Filled {results['text_fields_filled']} text fields, "
|
logger.info(f"Filled {results['text_fields_filled']} text fields, "
|
||||||
f"{results['combobox_fields_filled']} combobox fields, "
|
f"{results['combobox_fields_filled']} combobox fields, "
|
||||||
f"{results['checkboxes_set']} checkboxes")
|
f"{results['checkboxes_set']} checkboxes")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _fill_text_fields(self, rack_data: EditRackFormData, results: Dict[str, int]) -> None:
|
|
||||||
"""Заполняет текстовые поля.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items():
|
|
||||||
value = getattr(rack_data, attr_name, "")
|
|
||||||
if not value or not str(value).strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._fill_single_text_field(field_label, field_name, value, results)
|
|
||||||
|
|
||||||
def _fill_single_text_field(
|
|
||||||
self, field_label: str, field_name: str, value: str, results: Dict[str, int]
|
|
||||||
) -> None:
|
|
||||||
"""Заполняет одно текстовое поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
field_name: Имя поля
|
|
||||||
value: Значение для заполнения
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
input_field = self.get_content_item(field_name)
|
|
||||||
if input_field:
|
|
||||||
input_field.input_value(value)
|
|
||||||
results["text_fields_filled"] += 1
|
|
||||||
logger.debug(f"Field '{field_label}' filled: '{value}'")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error filling field '{field_label}': {e}")
|
|
||||||
|
|
||||||
def _fill_combobox_fields(self, rack_data: EditRackFormData, results: Dict[str, int]) -> None:
|
|
||||||
"""Заполняет combobox поля.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
|
||||||
value = getattr(rack_data, attr_name, "")
|
|
||||||
if not value or not str(value).strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._fill_single_combobox_field(
|
|
||||||
field_label, input_name, list_name, value, results
|
|
||||||
)
|
|
||||||
|
|
||||||
def _fill_single_combobox_field(
|
|
||||||
self, field_label: str, input_name: str, list_name: str, value: str, results: Dict[str, int]
|
|
||||||
) -> None:
|
|
||||||
"""Заполняет одно combobox поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Метка поля
|
|
||||||
input_name: Имя поля ввода
|
|
||||||
list_name: Имя списка
|
|
||||||
value: Значение для выбора
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
combobox_field = self.get_content_item(input_name)
|
|
||||||
if not combobox_field:
|
|
||||||
logger.warning(f"Field '{field_label}' input not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Кликаем для открытия выпадающего списка
|
|
||||||
combobox_field.click(force=True)
|
|
||||||
self.wait_for_timeout(1000)
|
|
||||||
|
|
||||||
# Скроллим до нужного элемента
|
|
||||||
if not self._scroll_to_element_in_dropdown(value):
|
|
||||||
logger.error(f"Could not find element with text '{value}' after scrolling")
|
|
||||||
# Закрываем выпадающий список кликом вне
|
|
||||||
self.page.mouse.click(10, 10)
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Получаем активное выпадающее меню
|
|
||||||
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
|
||||||
|
|
||||||
# Ищем элемент с нужным текстом
|
|
||||||
item_locator = dropdown_menu.locator(f"a.v-list__tile:has-text('{value}')").first
|
|
||||||
|
|
||||||
if item_locator.count() == 0:
|
|
||||||
item_locator = dropdown_menu.locator(f"span:has-text('{value}')").first
|
|
||||||
|
|
||||||
if item_locator.count() == 0:
|
|
||||||
item_locator = dropdown_menu.locator(f"div[role='listitem']:has-text('{value}')").first
|
|
||||||
|
|
||||||
if item_locator.count() > 0:
|
|
||||||
# Убеждаемся что элемент видим и кликаем
|
|
||||||
item_locator.scroll_into_view_if_needed()
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
item_locator.click()
|
|
||||||
results["combobox_fields_filled"] += 1
|
|
||||||
logger.debug(f"Field '{field_label}' set: '{value}'")
|
|
||||||
|
|
||||||
# Небольшая пауза после выбора
|
|
||||||
self.wait_for_timeout(500)
|
|
||||||
else:
|
|
||||||
logger.error(f"Item with text '{value}' not found in dropdown for field '{field_label}'")
|
|
||||||
# Закрываем выпадающий список кликом вне
|
|
||||||
self.page.mouse.click(10, 10)
|
|
||||||
self.wait_for_timeout(300)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error filling combobox '{field_label}': {e}")
|
|
||||||
self.page.mouse.click(10, 10)
|
|
||||||
|
|
||||||
def _set_checkbox(self, rack_data: EditRackFormData, results: Dict[str, int]) -> None:
|
|
||||||
"""Устанавливает чекбокс.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
results: Словарь с результатами
|
|
||||||
"""
|
|
||||||
|
|
||||||
if rack_data.ventilation_panel is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
checkbox = self.get_content_item("ventilation_checkbox")
|
|
||||||
if not checkbox:
|
|
||||||
logger.warning("Ventilation panel checkbox not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
if rack_data.ventilation_panel:
|
|
||||||
checkbox.check(force=True)
|
|
||||||
logger.debug("Ventilation panel checkbox checked")
|
|
||||||
else:
|
|
||||||
checkbox.uncheck(force=True)
|
|
||||||
logger.debug("Ventilation panel checkbox unchecked")
|
|
||||||
|
|
||||||
results["checkboxes_set"] += 1
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error setting checkbox: {e}")
|
|
||||||
|
|
||||||
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
|
||||||
"""Проверяет, подсвечено ли поле как ошибочное.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_name: Название поля для проверки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если поле подсвечено ошибкой, False в противном случае
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Для чекбокса проверка ошибок не применяется
|
|
||||||
if field_name == "Вентиляционная панель":
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Проверяем в текстовых полях
|
|
||||||
if field_name in self.TEXT_FIELDS_LOCATORS:
|
|
||||||
locator = self.TEXT_FIELDS_LOCATORS[field_name]
|
|
||||||
field_element = self.page.locator(locator).first
|
|
||||||
|
|
||||||
if field_element.count() == 0:
|
|
||||||
logger.debug(f"Field '{field_name}' not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Поднимаемся до родительского контейнера с классом v-input
|
|
||||||
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
|
|
||||||
|
|
||||||
if parent_input.count() > 0:
|
|
||||||
# Проверяем наличие класса ошибки
|
|
||||||
class_attr = parent_input.get_attribute("class") or ""
|
|
||||||
is_error = "v-input--error" in class_attr or "error--text" in class_attr
|
|
||||||
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
|
|
||||||
return is_error
|
|
||||||
|
|
||||||
# Проверяем в combobox полях
|
|
||||||
elif field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
|
||||||
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
|
|
||||||
field_element = self.page.locator(locator).first
|
|
||||||
|
|
||||||
if field_element.count() == 0:
|
|
||||||
logger.debug(f"Field '{field_name}' not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Поднимаемся до родительского контейнера с классом v-input
|
|
||||||
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
|
|
||||||
|
|
||||||
if parent_input.count() > 0:
|
|
||||||
# Проверяем наличие класса ошибки
|
|
||||||
class_attr = parent_input.get_attribute("class") or ""
|
|
||||||
is_error = "v-input--error" in class_attr or "error--text" in class_attr
|
|
||||||
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
|
|
||||||
return is_error
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def verify_required_fields_highlighted(self, field_names: List[str]) -> Dict[str, bool]:
|
|
||||||
"""Проверяет, что указанные поля подсвечены как обязательные (с ошибкой).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_names: Список названий полей для проверки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Словарь с результатами проверки {field_name: is_highlighted}
|
|
||||||
"""
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
for field_name in field_names:
|
|
||||||
results[field_name] = self.is_field_highlighted_as_error(field_name)
|
|
||||||
logger.debug(f"Field '{field_name}' highlighted: {results[field_name]}")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
|
||||||
"""Ожидает появления подсветки ошибки на поле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_name: Название поля
|
|
||||||
timeout: Таймаут в миллисекундах
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если ошибка появилась, False в противном случае
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Для чекбокса не ждем ошибок
|
|
||||||
if field_name == "Вентиляционная панель":
|
|
||||||
return False
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
while (time.time() - start_time) * 1000 < timeout:
|
|
||||||
if self.is_field_highlighted_as_error(field_name):
|
|
||||||
return True
|
|
||||||
self.wait_for_timeout(200)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_field_value(self, field_name: str) -> Optional[str]:
|
|
||||||
"""Получает значение поля.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_name: Название поля
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Значение поля или None если поле не найдено
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Для чекбокса
|
|
||||||
if field_name == "Вентиляционная панель":
|
|
||||||
checkbox = self.get_content_item("ventilation_checkbox")
|
|
||||||
if checkbox:
|
|
||||||
return str(checkbox.is_checked())
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Для текстовых полей
|
|
||||||
if field_name in self.TEXT_FIELDS_LOCATORS:
|
|
||||||
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
|
|
||||||
if attr_name == field_name or field_label == field_name:
|
|
||||||
input_field = self.get_content_item(widget_name)
|
|
||||||
if input_field:
|
|
||||||
return input_field.get_input_value()
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Для combobox полей
|
|
||||||
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
|
||||||
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_name)
|
|
||||||
if not locator:
|
|
||||||
# Пробуем найти по атрибуту
|
|
||||||
for field_label, (attr_name, input_name, _) in self.COMBOBOX_FIELDS_MAPPING.items():
|
|
||||||
if attr_name == field_name or field_label == field_name:
|
|
||||||
input_field = self.get_content_item(input_name)
|
|
||||||
if input_field:
|
|
||||||
# Получаем текст из поля
|
|
||||||
element = input_field.element
|
|
||||||
selections = element.locator("xpath=ancestor::div[contains(@class, 'v-select__selections')]").first
|
|
||||||
if selections.count() > 0:
|
|
||||||
value_span = selections.locator("span").first
|
|
||||||
return value_span.text_content() or ""
|
|
||||||
return None
|
|
||||||
|
|
||||||
element = self.page.locator(locator).first
|
|
||||||
if element.count() > 0:
|
|
||||||
selections = element.locator("xpath=ancestor::div[contains(@class, 'v-select__selections')]").first
|
|
||||||
if selections.count() > 0:
|
|
||||||
value_span = selections.locator("span").first
|
|
||||||
return value_span.text_content() or ""
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,6 +1,7 @@
|
||||||
"""Модуль фрейма создания дочернего элемента."""
|
"""Модуль фрейма создания дочернего элемента."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
from playwright.sync_api import Page, Locator
|
from playwright.sync_api import Page, Locator
|
||||||
from tools.logger import get_logger
|
from tools.logger import get_logger
|
||||||
from locators.rack_locators import RackLocators
|
from locators.rack_locators import RackLocators
|
||||||
|
|
@ -9,6 +10,7 @@ from components.alert_component import AlertComponent
|
||||||
from components.base_component import BaseComponent
|
from components.base_component import BaseComponent
|
||||||
from components.toolbar_component import ToolbarComponent
|
from components.toolbar_component import ToolbarComponent
|
||||||
from components_derived.selection_bar_component import SelectionBarComponent
|
from components_derived.selection_bar_component import SelectionBarComponent
|
||||||
|
from forms.create_rack_form import CreateRackForm, CreateRackData
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("CREATE_ELEMENT_FRAME")
|
logger = get_logger("CREATE_ELEMENT_FRAME")
|
||||||
|
|
@ -27,6 +29,9 @@ class CreateElementFrame(BaseComponent):
|
||||||
"""
|
"""
|
||||||
super().__init__(page)
|
super().__init__(page)
|
||||||
|
|
||||||
|
# Инициализация формы создания стойки
|
||||||
|
self.rack_form = CreateRackForm(page)
|
||||||
|
|
||||||
# Инициализация компонентов
|
# Инициализация компонентов
|
||||||
self.toolbar = ToolbarComponent(page, "Создать дочерний элемент в")
|
self.toolbar = ToolbarComponent(page, "Создать дочерний элемент в")
|
||||||
self.selection_bar = SelectionBarComponent(page, "Класс объекта учета")
|
self.selection_bar = SelectionBarComponent(page, "Класс объекта учета")
|
||||||
|
|
@ -46,7 +51,67 @@ class CreateElementFrame(BaseComponent):
|
||||||
self.toolbar.add_tooltip_button(add_button_locator, "add")
|
self.toolbar.add_tooltip_button(add_button_locator, "add")
|
||||||
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
|
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
|
||||||
|
|
||||||
# Действия:
|
# Делегирование методов форме создания стойки
|
||||||
|
|
||||||
|
def fill_rack_data(self, rack_data: CreateRackData) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Заполняет поля формы создания стойки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rack_data: Данные для заполнения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь с результатами заполнения
|
||||||
|
"""
|
||||||
|
return self.rack_form.fill_rack_data(rack_data)
|
||||||
|
|
||||||
|
def clear_field(self, field_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Очищает указанное поле формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для очистки
|
||||||
|
"""
|
||||||
|
self.rack_form.clear_field(field_name)
|
||||||
|
|
||||||
|
def get_field_value(self, field_name: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получает значение поля формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Значение поля или None если поле не найдено
|
||||||
|
"""
|
||||||
|
return self.rack_form.get_field_value(field_name)
|
||||||
|
|
||||||
|
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, подсвечено ли поле как ошибочное.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если поле подсвечено ошибкой
|
||||||
|
"""
|
||||||
|
return self.rack_form.is_field_highlighted_as_error(field_name)
|
||||||
|
|
||||||
|
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
||||||
|
"""
|
||||||
|
Ожидает появления подсветки ошибки на поле.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля
|
||||||
|
timeout: Таймаут в миллисекундах
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если ошибка появилась
|
||||||
|
"""
|
||||||
|
return self.rack_form.wait_for_field_error(field_name, timeout)
|
||||||
|
|
||||||
|
# Оригинальные методы фрейма
|
||||||
|
|
||||||
def clear_combobox_field(self, field_name: str) -> None:
|
def clear_combobox_field(self, field_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -192,22 +257,9 @@ class CreateElementFrame(BaseComponent):
|
||||||
AssertionError: Если поле не подсвечено ошибкой
|
AssertionError: Если поле не подсвечено ошибкой
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Checking field '{field_name}' for error highlighting...")
|
logger.debug(f"Checking field '{field_name}' for error highlighting...")
|
||||||
|
assert self.is_field_highlighted_as_error(field_name), (
|
||||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
|
f"Field '{field_name}' is not highlighted as error"
|
||||||
fields_locators = self.get_input_fields_locators(container_locator)
|
|
||||||
field_container = fields_locators.get(field_name)
|
|
||||||
|
|
||||||
if not field_container:
|
|
||||||
raise ValueError(f"Field '{field_name}' not found in form")
|
|
||||||
|
|
||||||
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
|
|
||||||
has_error = error_elements.count() > 0
|
|
||||||
|
|
||||||
assert has_error, (
|
|
||||||
f"Field '{field_name}' has no elements with error classes. "
|
|
||||||
f"Expected to find elements matching: {SelectionBarLocators.ERROR_CSS_SELECTORS}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Field '{field_name}' is correctly highlighted with error color")
|
logger.debug(f"Field '{field_name}' is correctly highlighted with error color")
|
||||||
|
|
||||||
def check_field_error_not_highlighted(self, field_name: str) -> None:
|
def check_field_error_not_highlighted(self, field_name: str) -> None:
|
||||||
|
|
@ -222,22 +274,9 @@ class CreateElementFrame(BaseComponent):
|
||||||
AssertionError: Если поле подсвечено ошибкой
|
AssertionError: Если поле подсвечено ошибкой
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Checking field '{field_name}' for absence of error highlighting...")
|
logger.debug(f"Checking field '{field_name}' for absence of error highlighting...")
|
||||||
|
assert not self.is_field_highlighted_as_error(field_name), (
|
||||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
|
f"Field '{field_name}' is incorrectly highlighted as error"
|
||||||
fields_locators = self.get_input_fields_locators(container_locator)
|
|
||||||
field_container = fields_locators.get(field_name)
|
|
||||||
|
|
||||||
if not field_container:
|
|
||||||
raise ValueError(f"Field '{field_name}' not found in form")
|
|
||||||
|
|
||||||
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
|
|
||||||
has_error = error_elements.count() > 0
|
|
||||||
|
|
||||||
assert not has_error, (
|
|
||||||
f"Field '{field_name}' has {error_elements.count()} elements with error classes. "
|
|
||||||
f"Expected no elements matching: {SelectionBarLocators.ERROR_CSS_SELECTORS}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Field '{field_name}' correctly has no error highlighting")
|
logger.debug(f"Field '{field_name}' correctly has no error highlighting")
|
||||||
|
|
||||||
def check_object_class_selected(self, expected_class: str) -> None:
|
def check_object_class_selected(self, expected_class: str) -> None:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
"""Модуль backup_tab_locators содержит локаторы элементов страницы 'Резервное копирование'.
|
||||||
|
|
||||||
|
Класс RackLocators хранит XPath/CSS локаторы для взаимодействия
|
||||||
|
с элементами интерфейса вкладки в тестах.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
class BackupTabLocators:
|
||||||
|
"""Класс для хранения локаторов элементов страницы 'Резервное копирование'.
|
||||||
|
|
||||||
|
Содержит локаторы в формате XPath/CSS для поиска элементов
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Кнопки на тулбаре
|
||||||
|
BUTTON_EDIT_TOOLBAR = "//button[@data-testid='BACKUP_PANEL__btn__edit']"
|
||||||
|
BUTTON_SAVE_TOOLBAR = "//button[@data-testid='BACKUP_PANEL__btn__submit']"
|
||||||
|
BUTTON_CANCEL_TOOLBAR = "//button[@data-testid='BACKUP_PANEL__btn__cancel']"
|
||||||
|
|
||||||
|
# Кнопки раздела 'Инвентаризация'
|
||||||
|
BUTTON_INVENTORY_CREATE_COPY = "//button[@data-testid='BACKUP_PANEL__btn__createCopy_cmdb']"
|
||||||
|
BUTTON_INVENTORY_UPLOAD_COPY = "//button[@data-testid='BACKUP_PANEL__btn__upload_cmdb']"
|
||||||
|
BUTTON_INVENTORY_RESTORE_COPY = "//button[@data-testid='BACKUP_PANEL__btn__restore_cmdb']"
|
||||||
|
BUTTON_INVENTORY_DOWNLOAD_COPY = "//button[@data-testid='BACKUP_PANEL__btn__download_cmdb']"
|
||||||
|
|
||||||
|
# Набор полей 'Инвентаризация/Параметры планировщика'
|
||||||
|
INPUT_INVENTORY_BACKUP_CREATION_TIME = "//input[@data-testid='BACKUP_PANEL__text-field__auto_backup_cmdb']"
|
||||||
|
INPUT_INVENTORY_BACKUP_NUMBERS = "//input[@data-testid='BACKUP_PANEL__text-field__backup_limitation_cmdb']"
|
||||||
|
|
||||||
|
# Кнопки раздела 'Потоковые данные'
|
||||||
|
BUTTON_STREAMING_DATA_CREATE_COPY = "//button[@data-testid='BACKUP_PANEL__btn__createCopy_streaming_data']"
|
||||||
|
BUTTON_STREAMING_DATA_UPLOAD_COPY = "//button[@data-testid='BACKUP_PANEL__btn__upload_streaming_data']"
|
||||||
|
BUTTON_STREAMING_DATA_RESTORE_COPY = "//button[@data-testid='BACKUP_PANEL__btn__restore_streaming_data']"
|
||||||
|
BUTTON_STREAMING_DATA_DOWNLOAD_COPY = "//button[@data-testid='BACKUP_PANEL__btn__download_streaming_data']"
|
||||||
|
|
||||||
|
# Поля ввода данных для различных категорий раздела 'Потоковые данные'
|
||||||
|
INPUT_AUDIT_TIME_PERIOD = "//input[@data-testid='BACKUP_PANEL__text-field__data_limitation_default_audit']"
|
||||||
|
INPUT_AUDIT_TIME_PERIOD_INTERVAL = "//input[@data-testid='BACKUP_PANEL__select__interval_limitation_default_audit']"
|
||||||
|
INPUT_LOGS_TIME_PERIOD = "//input[@data-testid='BACKUP_PANEL__text-field__data_limitation_logs_logs']"
|
||||||
|
INPUT_LOGS_TIME_PERIOD_INTERVAL ="//input[@data-testid='BACKUP_PANEL__select__interval_limitation_logs_logs']"
|
||||||
|
INPUT_METRICS_TIME_PERIOD = "//input[@data-testid='BACKUP_PANEL__text-field__data_limitation_metrics_metrics']"
|
||||||
|
INPUT_METRICS_TIME_PERIOD_INTERVAL = "//input[@data-testid='BACKUP_PANEL__select__interval_limitation_metrics_metrics']"
|
||||||
|
INPUT_SYSLOG_TIME_PERIOD = "//input[@data-testid='BACKUP_PANEL__text-field__data_limitation_syslog_syslog']"
|
||||||
|
INPUT_SYSLOG_TIME_PERIOD_INTERVAL = "//input[@data-testid='BACKUP_PANEL__select__interval_limitation_syslog_syslog']"
|
||||||
|
INPUT_TASKS_TIME_PERIOD = "//input[@data-testid='BACKUP_PANEL__text-field__data_limitation_tasks_tasks']"
|
||||||
|
INPUT_TASKS_TIME_PERIOD_INTERVAL = "//input[@data-testid='BACKUP_PANEL__select__interval_limitation_tasks_tasks']"
|
||||||
|
|
||||||
|
# Набор полей 'Потоковые данные/Параметры планировщика'
|
||||||
|
INPUT_STREAMING_DATA_BACKUP_CREATION_TIME = "//input[@data-testid='BACKUP_PANEL__text-field__auto_backup_streaming_data']"
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
"""Модуль certificate_locators содержит локаторы элементов вкладки 'Сертификаты'.
|
||||||
|
|
||||||
|
Класс ToolbarLocators предоставляет XPath локаторы для взаимодействия
|
||||||
|
с элементами тулбара и всплывающими подсказками.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CertificateLocators:
|
||||||
|
"""Локаторы элементов вкладки 'Сертификаты'.
|
||||||
|
|
||||||
|
Содержит XPath локаторы для поиска элементов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TOOLBAR_CASTOM = "//div[contains(@class, 'scrollarea__container')]//div[contains(@class,'toolbar_castom')]"
|
||||||
|
|
||||||
|
MAIN_CONTAINER = f"{TOOLBAR_CASTOM}/ancestor::div[4]"
|
||||||
|
MAIN_CONTAINER_HEADER = f"{MAIN_CONTAINER}//div[contains(@class, 'scrollarea__header')]"
|
||||||
|
MAIN_CONTAINER_BODY = f"{MAIN_CONTAINER}//div[contains(@class, 'scrollarea__body')]"
|
||||||
|
|
||||||
|
TAB_CERTIFICATE_CA = f"{MAIN_CONTAINER_HEADER}//a[contains(@class, 'v-tabs__item') and contains(.,'Сертификат CA')]"
|
||||||
|
TAB_REISSUE_CA = f"{MAIN_CONTAINER_HEADER}//a[contains(@class, 'v-tabs__item') and contains(., 'Пересоздание CA')]"
|
||||||
|
TAB_IMPORT_CA = f"{MAIN_CONTAINER_HEADER}//a[contains(@class, 'v-tabs__item') and contains(., 'import ca (p12)')]"
|
||||||
|
|
||||||
|
FORM_CONTAINER = f"{MAIN_CONTAINER_BODY}//div[contains(@class, 'scrollarea__body')]"
|
||||||
|
|
||||||
|
BLOCK_HEADER_TEXT = f"{FORM_CONTAINER}//span[@class='body-2']"
|
||||||
|
|
||||||
|
# поля блока 'Сертификат CA/Основная информация'
|
||||||
|
FIELD_VERSION = "//input[@data-testid='SERTIFICATES-CA__text-field__baseInfo.version']"
|
||||||
|
FIELD_SERIAL_NUMBER = "//input[@data-testid='SERTIFICATES-CA__text-field__baseInfo.serialNumber']"
|
||||||
|
FIELD_SIGNATURE_ALGORITHM = "//input[@data-testid='SERTIFICATES-CA__text-field__baseInfo.signatureAlgorithm']"
|
||||||
|
|
||||||
|
# поля блока 'Сертификат CA/Срок действия'
|
||||||
|
FIELD_VALIDITY = "//input[@data-testid='SERTIFICATES-CA__text-field__validity.status']"
|
||||||
|
FIELD_NOT_BEFORE = "//input[@data-testid='SERTIFICATES-CA__text-field__validity.notBefore']"
|
||||||
|
FIELD_NOT_AFTER = "//input[@data-testid='SERTIFICATES-CA__text-field__validity.notAfter']"
|
||||||
|
|
||||||
|
# поля блока 'Сертификат CA/Издатель / Субъект'
|
||||||
|
FIELD_CERT_NAME = "//input[@data-testid='SERTIFICATES-CA__text-field__subject.CN']"
|
||||||
|
FIELD_ORGANIZATION = "//input[@data-testid='SERTIFICATES-CA__text-field__subject.O']"
|
||||||
|
FIELD_ORG_UNIT = "//input[@data-testid='SERTIFICATES-CA__text-field__subject.OU']"
|
||||||
|
FIELD_COUNTRY = "//input[@data-testid='SERTIFICATES-CA__text-field__subject.C']"
|
||||||
|
FIELD_STATE = "//input[@data-testid='SERTIFICATES-CA__text-field__subject.ST']"
|
||||||
|
FIELD_LOC = "//input[@data-testid='SERTIFICATES-CA__text-field__subject.L']"
|
||||||
|
|
||||||
|
# поля блока 'Сертификат CA/Ключ и отпечаток'
|
||||||
|
FIELD_PUBLIC_KEY_FINGERPRINT = "//input[@data-testid='SERTIFICATES-CA__text-field__fingerprint.publicKeyFingerprint']"
|
||||||
|
FIELD_ALGORITHM = "//input[@data-testid='SERTIFICATES-CA__text-field__fingerprint.algorithm']"
|
||||||
|
FIELD_KEY_SIZE = "//input[@data-testid='SERTIFICATES-CA__text-field__fingerprint.keySize']"
|
||||||
|
|
||||||
|
# поля блока 'Пересоздание CA/Идентификация CA'
|
||||||
|
FIELD_INPUT_CERT_NAME = "//input[@data-testid='SERTIFICATES-REISSUE__text-field__publisher.cn']"
|
||||||
|
FIELD_INPUT_ORGANIZATION = "//input[@data-testid='SERTIFICATES-REISSUE__text-field__publisher.o']"
|
||||||
|
FIELD_INPUT_ORG_UNIT = "//input[@data-testid='SERTIFICATES-REISSUE__text-field__publisher.ou']"
|
||||||
|
|
||||||
|
# поля блока 'Пересоздание CA/Адрес / Местонахождение'
|
||||||
|
FIELD_INPUT_COUNTRY = "//input[@data-testid='SERTIFICATES-REISSUE__text-field__publisher.c']"
|
||||||
|
FIELD_INPUT_STATE = "//input[@data-testid='SERTIFICATES-REISSUE__text-field__publisher.st']"
|
||||||
|
FIELD_INPUT_LOC = "//input[@data-testid='SERTIFICATES-REISSUE__text-field__publisher.l']"
|
||||||
|
|
||||||
|
# поля блока 'Импорт CA'
|
||||||
|
FIELD_INPUT_PASSWORD = "//input[@data-testid='SERTIFICATES-IMPORT__text-field__pass']"
|
||||||
|
BUTTON_IMPORT = "//button[@data-testid='SERTIFICATES-IMPORT__btn__upload_p12']"
|
||||||
|
|
||||||
|
|
@ -13,4 +13,4 @@ class JsonContainerLocators:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CONTAINER = "//div[contains(@class,'jv-container')]"
|
CONTAINER = "//div[contains(@class,'jv-container')]"
|
||||||
SCROLL_CONTAINER = "//div[contains(@class, 'scrollarea__body')]"
|
SCROLL_CONTAINER = "//nav[contains(@class, 'active v-toolbar')]/../following-sibling::div//div[contains(@class,'scrollarea__body')]"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class SelectionBarLocators:
|
||||||
PARAMETERS_SELECTED = "div.v-select__selections"
|
PARAMETERS_SELECTED = "div.v-select__selections"
|
||||||
|
|
||||||
# Локаторы для элементов выпадающего списка
|
# Локаторы для элементов выпадающего списка
|
||||||
|
LIST_ACTIVE = "//div[contains(@class, 'menuable__content__active')]"
|
||||||
LISTBOX = "//div[@role='list']"
|
LISTBOX = "//div[@role='list']"
|
||||||
LIST_ITEMS = "//div[contains(@class, 'menuable__content__active')]//div[@role='list']//div[@role='listitem']"
|
LIST_ITEMS = "//div[contains(@class, 'menuable__content__active')]//div[@role='list']//div[@role='listitem']"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,3 +1,4 @@
|
||||||
|
# makers/edit_rack_maker.py
|
||||||
"""Модуль для работы с модальным окном редактирования стойки."""
|
"""Модуль для работы с модальным окном редактирования стойки."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
@ -9,17 +10,16 @@ from components.modal_window_component import ModalWindowComponent
|
||||||
from components.dropdown_list_component import DropdownList
|
from components.dropdown_list_component import DropdownList
|
||||||
from components.confirm_component import ConfirmComponent
|
from components.confirm_component import ConfirmComponent
|
||||||
from elements.text_input_element import TextInput
|
from elements.text_input_element import TextInput
|
||||||
from elements.text_element import Text
|
from forms.edit_rack_form import EditRackForm, EditRackData
|
||||||
from forms.edit_rack_form import EditRackForm, EditRackFormData
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("EDIT_RACK_MAKER")
|
logger = get_logger("EDIT_RACK_MAKER")
|
||||||
logger.setLevel("INFO")
|
logger.setLevel("INFO")
|
||||||
|
|
||||||
|
|
||||||
# Используем EditRackFormData
|
# Re-export EditRackData for backward compatibility
|
||||||
EditRackData = EditRackFormData
|
EditRackData = EditRackData
|
||||||
|
__all__ = ['EditRackMaker', 'EditRackData']
|
||||||
|
|
||||||
class EditRackMaker(ModalWindowComponent):
|
class EditRackMaker(ModalWindowComponent):
|
||||||
"""Компонент для работы с модальным окном редактирования стойки.
|
"""Компонент для работы с модальным окном редактирования стойки.
|
||||||
|
|
@ -38,7 +38,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
TAB_IMAGE = "Изображение"
|
TAB_IMAGE = "Изображение"
|
||||||
TAB_SETTINGS = "Настройки"
|
TAB_SETTINGS = "Настройки"
|
||||||
|
|
||||||
# Маппинг полей для вкладки "Настройки" - оставляем только то, что специфично для модального окна
|
# Маппинг полей для вкладки "Настройки"
|
||||||
ACCESS_RULES_MAPPING = {
|
ACCESS_RULES_MAPPING = {
|
||||||
"Правила доступа для чтения": (
|
"Правила доступа для чтения": (
|
||||||
"read_access_rules", "rules_read_input", "rules_read_list"
|
"read_access_rules", "rules_read_input", "rules_read_list"
|
||||||
|
|
@ -57,7 +57,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для полей правил доступа (из RackLocators)
|
# Локаторы для полей правил доступа
|
||||||
ACCESS_RULES_LOCATORS = {
|
ACCESS_RULES_LOCATORS = {
|
||||||
"Правила доступа для чтения": RackLocators.SETTINGS_READ_RULES,
|
"Правила доступа для чтения": RackLocators.SETTINGS_READ_RULES,
|
||||||
"Правила доступа для записи": RackLocators.SETTINGS_WRITE_RULES,
|
"Правила доступа для записи": RackLocators.SETTINGS_WRITE_RULES,
|
||||||
|
|
@ -210,6 +210,82 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
self.add_button(self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON), "cancel")
|
self.add_button(self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON), "cancel")
|
||||||
self.add_button(self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON), "delete")
|
self.add_button(self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON), "delete")
|
||||||
|
|
||||||
|
# Делегирование методов форме редактирования
|
||||||
|
|
||||||
|
def fill_rack_data(self, rack_data: EditRackData) -> dict:
|
||||||
|
"""Заполняет поля формы редактирования стойки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rack_data: Данные для заполнения.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь с результатами заполнения.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.active_tab != self.TAB_GENERAL:
|
||||||
|
self.switch_to_tab(self.TAB_GENERAL)
|
||||||
|
|
||||||
|
if not self.edit_form:
|
||||||
|
logger.error("Edit form not initialized")
|
||||||
|
return {
|
||||||
|
"text_fields_filled": 0,
|
||||||
|
"combobox_fields_filled": 0,
|
||||||
|
"checkboxes_set": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
results = self.edit_form.fill_rack_data(rack_data)
|
||||||
|
logger.info(f"Filled rack data via EditRackForm: {results}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
def clear_field(self, field_name: str) -> None:
|
||||||
|
"""Очищает указанное поле формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для очистки.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
self.edit_form.clear_field(field_name)
|
||||||
|
|
||||||
|
def get_field_value(self, field_name: str) -> Optional[str]:
|
||||||
|
"""Получает значение поля формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Значение поля или None если поле не найдено.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
return self.edit_form.get_field_value(field_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
||||||
|
"""Проверяет, подсвечено ли поле как ошибочное.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для проверки.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если поле подсвечено ошибкой.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
return self.edit_form.is_field_highlighted_as_error(field_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
||||||
|
"""Ожидает появления подсветки ошибки на поле.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля.
|
||||||
|
timeout: Таймаут в миллисекундах.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если ошибка появилась.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
return self.edit_form.wait_for_field_error(field_name, timeout)
|
||||||
|
return False
|
||||||
|
|
||||||
# Действия с вкладками
|
# Действия с вкладками
|
||||||
def switch_to_tab(self, tab_name: str) -> None:
|
def switch_to_tab(self, tab_name: str) -> None:
|
||||||
"""Переключается на указанную вкладку.
|
"""Переключается на указанную вкладку.
|
||||||
|
|
@ -348,11 +424,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
target_fields: Список целевых полей для заполнения.
|
target_fields: Список целевых полей для заполнения.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Словарь с результатами заполнения:
|
Словарь с результатами заполнения.
|
||||||
- access_rules_filled: количество добавленных пользователей
|
|
||||||
- errors: список ошибок
|
|
||||||
- fields_processed: обработанные поля
|
|
||||||
- field_stats: статистика по каждому полю
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.active_tab != self.TAB_SETTINGS:
|
if self.active_tab != self.TAB_SETTINGS:
|
||||||
|
|
@ -574,28 +646,52 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
username: str,
|
username: str,
|
||||||
field_label: str
|
field_label: str
|
||||||
) -> Tuple[bool, Optional[str]]:
|
) -> Tuple[bool, Optional[str]]:
|
||||||
"""Добавляет пользователя из выпадающего списка.
|
"""Добавляет пользователя из выпадающего списка."""
|
||||||
|
|
||||||
Args:
|
|
||||||
dropdown_menu: Выпадающее меню.
|
|
||||||
username: Имя пользователя.
|
|
||||||
field_label: Название поля.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Кортеж (добавлен ли пользователь, сообщение об ошибке или None).
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_item = dropdown_menu.locator(f"[role='listitem']:has-text('{username}')").first
|
# Получаем все элементы списка
|
||||||
if user_item.count() == 0:
|
listitem_locator = dropdown_menu.get_by_role("listitem")
|
||||||
user_item = dropdown_menu.locator(f"div:has-text('{username}')").first
|
|
||||||
|
|
||||||
if user_item.count() > 0:
|
# Проверяем, что элементы есть
|
||||||
user_item.click()
|
if listitem_locator.count() == 0:
|
||||||
self.wait_for_timeout(500)
|
return False, f"No list items found in dropdown for {field_label}"
|
||||||
return True, None
|
|
||||||
|
# Прокручиваем к последнему элементу
|
||||||
|
listitem_locator.last.scroll_into_view_if_needed()
|
||||||
|
# Ждем, пока последний элемент станет видимым
|
||||||
|
listitem_locator.last.wait_for(state="visible")
|
||||||
|
|
||||||
|
# Ищем элемент с точным совпадением текста
|
||||||
|
all_items = listitem_locator.all()
|
||||||
|
target_item = None
|
||||||
|
|
||||||
|
for item in all_items:
|
||||||
|
# Ищем span с текстом внутри элемента
|
||||||
|
span = item.locator("span").first
|
||||||
|
if span.inner_text().strip() == username:
|
||||||
|
target_item = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if target_item is None:
|
||||||
|
return False, f"User '{username}' not found in dropdown for {field_label}"
|
||||||
|
|
||||||
|
# Проверяем, не выбран ли уже этот пользователь
|
||||||
|
class_attribute = target_item.get_attribute("class") or ""
|
||||||
|
|
||||||
|
# Если элемент уже выбран (есть класс v-list__tile--active)
|
||||||
|
if "v-list__tile--active" in class_attribute:
|
||||||
|
logger.debug(f"User '{username}' is already selected in {field_label}, skipping")
|
||||||
|
return True, None # Считаем как успех, т.к. пользователь уже есть
|
||||||
|
|
||||||
|
# Прокручиваем к найденному элементу
|
||||||
|
target_item.scroll_into_view_if_needed()
|
||||||
|
target_item.wait_for(state="visible")
|
||||||
|
|
||||||
|
# Кликаем
|
||||||
|
target_item.click()
|
||||||
|
self.wait_for_timeout(500)
|
||||||
|
return True, None
|
||||||
|
|
||||||
return False, f"User '{username}' not found in dropdown for {field_label}"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"Failed to add user '{username}' to {field_label}: {str(e)}"
|
return False, f"Failed to add user '{username}' to {field_label}: {str(e)}"
|
||||||
|
|
||||||
|
|
@ -632,13 +728,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
target_fields: Список целевых полей для проверки.
|
target_fields: Список целевых полей для проверки.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Словарь с результатами проверки:
|
Словарь с результатами проверки.
|
||||||
- total_expected_fields: общее количество ожидаемых значений
|
|
||||||
- correctly_filled: количество корректно заполненных
|
|
||||||
- incorrectly_filled: количество некорректно заполненных
|
|
||||||
- field_errors: список ошибок по полям
|
|
||||||
- fields_verified: список проверенных полей
|
|
||||||
- expected_users: список ожидаемых пользователей
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.active_tab != self.TAB_SETTINGS:
|
if self.active_tab != self.TAB_SETTINGS:
|
||||||
|
|
@ -859,32 +949,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
save_button.click()
|
save_button.click()
|
||||||
logger.debug("Clicked done button")
|
logger.debug("Clicked done button")
|
||||||
|
|
||||||
def fill_rack_data(self, rack_data: EditRackData) -> dict:
|
|
||||||
"""Заполняет поля формы редактирования стойки.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Словарь с результатами заполнения.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.active_tab != self.TAB_GENERAL:
|
|
||||||
self.switch_to_tab(self.TAB_GENERAL)
|
|
||||||
|
|
||||||
# Используем форму для заполнения данных
|
|
||||||
if self.edit_form:
|
|
||||||
results = self.edit_form.fill_rack_data(rack_data)
|
|
||||||
logger.info(f"Filled rack data via EditRackForm: {results}")
|
|
||||||
return results
|
|
||||||
else:
|
|
||||||
logger.error("Edit form not initialized")
|
|
||||||
return {
|
|
||||||
"text_fields_filled": 0,
|
|
||||||
"combobox_fields_filled": 0,
|
|
||||||
"checkboxes_set": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверки
|
# Проверки
|
||||||
def verify_all_filled_fields(
|
def verify_all_filled_fields(
|
||||||
self,
|
self,
|
||||||
|
|
@ -916,6 +980,11 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
if skip_fields is None:
|
if skip_fields is None:
|
||||||
skip_fields = []
|
skip_fields = []
|
||||||
|
|
||||||
|
if not self.edit_form:
|
||||||
|
logger.error("Edit form not initialized")
|
||||||
|
results["field_errors"].append("Edit form not initialized")
|
||||||
|
return results
|
||||||
|
|
||||||
# Проверяем текстовые поля
|
# Проверяем текстовые поля
|
||||||
self._verify_text_fields(rack_data, skip_fields, results)
|
self._verify_text_fields(rack_data, skip_fields, results)
|
||||||
|
|
||||||
|
|
@ -948,10 +1017,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
results: Словарь с результатами для обновления.
|
results: Словарь с результатами для обновления.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.edit_form:
|
|
||||||
logger.error("Edit form not initialized")
|
|
||||||
return
|
|
||||||
|
|
||||||
for field_label, (attr_name, field_name) in self.edit_form.TEXT_FIELDS_MAPPING.items():
|
for field_label, (attr_name, field_name) in self.edit_form.TEXT_FIELDS_MAPPING.items():
|
||||||
expected_value = getattr(rack_data, attr_name, "")
|
expected_value = getattr(rack_data, attr_name, "")
|
||||||
if not expected_value or not str(expected_value).strip():
|
if not expected_value or not str(expected_value).strip():
|
||||||
|
|
@ -1014,10 +1079,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
results: Словарь с результатами для обновления.
|
results: Словарь с результатами для обновления.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.edit_form:
|
|
||||||
logger.error("Edit form not initialized")
|
|
||||||
return
|
|
||||||
|
|
||||||
for field_label, (attr_name, _, _) in self.edit_form.COMBOBOX_FIELDS_MAPPING.items():
|
for field_label, (attr_name, _, _) in self.edit_form.COMBOBOX_FIELDS_MAPPING.items():
|
||||||
expected_value = getattr(rack_data, attr_name, "")
|
expected_value = getattr(rack_data, attr_name, "")
|
||||||
if not expected_value or not str(expected_value).strip():
|
if not expected_value or not str(expected_value).strip():
|
||||||
|
|
@ -1046,9 +1107,9 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actual_value = self._get_combobox_value(field_label)
|
actual_value = self.edit_form.get_field_value(field_label) or ""
|
||||||
actual_clean = actual_value.strip() if actual_value else ""
|
actual_clean = actual_value.strip()
|
||||||
expected_clean = expected_value.strip() if expected_value else ""
|
expected_clean = expected_value.strip()
|
||||||
|
|
||||||
if actual_clean == expected_clean:
|
if actual_clean == expected_clean:
|
||||||
results["correctly_filled"] += 1
|
results["correctly_filled"] += 1
|
||||||
|
|
@ -1061,40 +1122,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
results["not_filled"] += 1
|
results["not_filled"] += 1
|
||||||
results["field_errors"].append(f"Error checking combobox '{field_label}': {e}")
|
results["field_errors"].append(f"Error checking combobox '{field_label}': {e}")
|
||||||
|
|
||||||
def _get_combobox_value(self, field_label: str) -> str:
|
|
||||||
"""Получает значение из combobox поля.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
field_label: Название поля.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Значение поля или пустая строка.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self.edit_form:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
actual_value = ""
|
|
||||||
# Используем локаторы из edit_form
|
|
||||||
locator = self.edit_form.COMBOBOX_FIELDS_LOCATORS.get(field_label)
|
|
||||||
|
|
||||||
if not locator:
|
|
||||||
return actual_value
|
|
||||||
|
|
||||||
element = self.page.locator(locator).first
|
|
||||||
if element.count() == 0:
|
|
||||||
return actual_value
|
|
||||||
|
|
||||||
selections_container = element.locator(
|
|
||||||
"xpath=ancestor::div[contains(@class, 'v-select__selections')]"
|
|
||||||
).first
|
|
||||||
|
|
||||||
if selections_container.count() > 0:
|
|
||||||
value_span = selections_container.locator("span").first
|
|
||||||
actual_value = value_span.text_content() or ""
|
|
||||||
|
|
||||||
return actual_value
|
|
||||||
|
|
||||||
def _verify_checkbox(
|
def _verify_checkbox(
|
||||||
self,
|
self,
|
||||||
rack_data: EditRackData,
|
rack_data: EditRackData,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,178 @@
|
||||||
|
"""Модуль вкладки 'Сертификаты'.
|
||||||
|
|
||||||
|
Содержит класс CertificatesTab для работы с вкладкой 'Сертификаты'.
|
||||||
|
Позволяет проверять состояние и взаимодействовать с элементами вкладки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from playwright.sync_api import Page
|
||||||
|
from locators.certificate_locators import CertificateLocators
|
||||||
|
from elements.tab_button_element import TabButton
|
||||||
|
from components.toolbar_custom_component import CustomToolbar
|
||||||
|
from components.alert_component import AlertComponent
|
||||||
|
from components_derived.view_certificate_form import ViewCertificateForm
|
||||||
|
from components_derived.reissue_certificate_form import ReissueCertificateForm
|
||||||
|
from components_derived.import_certificate_form import ImportCertificateForm
|
||||||
|
from pages.base_page import BasePage
|
||||||
|
|
||||||
|
|
||||||
|
class CertificatesTab(BasePage):
|
||||||
|
"""Класс для работы с вкладкой 'Сертификаты'.
|
||||||
|
|
||||||
|
Предоставляет методы для взаимодействия с вкладкой 'Сертификаты'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Экземпляр страницы Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, page: Page) -> None:
|
||||||
|
"""Инициализирует компоненты вкладки настройки резервного копирования."""
|
||||||
|
|
||||||
|
super().__init__(page)
|
||||||
|
|
||||||
|
self.toolbar_main = CustomToolbar(page)
|
||||||
|
self.toolbar_secondary = CustomToolbar(page)
|
||||||
|
|
||||||
|
self.tab_button_certificate = TabButton(page, CertificateLocators.TAB_CERTIFICATE_CA, "tab_button_certificate")
|
||||||
|
self.tab_button_reissue = TabButton(page, CertificateLocators.TAB_REISSUE_CA, "tab_button_reissue")
|
||||||
|
self.tab_button_import = TabButton(page, CertificateLocators.TAB_IMPORT_CA, "tab_button_import")
|
||||||
|
|
||||||
|
self.view_certificate_form = ViewCertificateForm(page)
|
||||||
|
self.reissue_certificate_form = ReissueCertificateForm(page)
|
||||||
|
self.import_certificate_form = ImportCertificateForm(page)
|
||||||
|
|
||||||
|
self.alert = AlertComponent(page)
|
||||||
|
|
||||||
|
# Действия:
|
||||||
|
def click_certificate_tab_button(self) -> None:
|
||||||
|
"""Выполняет нажатие tab-кнопки 'Сертификат CA'."""
|
||||||
|
|
||||||
|
self.tab_button_certificate.check_visibility("'Сертификат CA' tab button is missing")
|
||||||
|
self.tab_button_certificate.click()
|
||||||
|
assert self.tab_button_certificate.is_active(), "'Сертификат CA' tab button should be active"
|
||||||
|
|
||||||
|
def click_reissue_tab_button(self) -> None:
|
||||||
|
"""Выполняет нажатие tab-кнопки 'Пересоздание CA'."""
|
||||||
|
|
||||||
|
self.tab_button_reissue.check_visibility("'Пересоздание CA' tab button is missing")
|
||||||
|
self.tab_button_reissue.click()
|
||||||
|
assert self.tab_button_reissue.is_active(), "'Пересоздание CA' tab button should be active"
|
||||||
|
|
||||||
|
def click_import_tab_button(self) -> None:
|
||||||
|
"""Выполняет нажатие tab-кнопки 'Import ca (p12)'."""
|
||||||
|
|
||||||
|
self.tab_button_import.check_visibility("'Import ca (p12)' tab button is missing")
|
||||||
|
self.tab_button_import.click()
|
||||||
|
assert self.tab_button_import.is_active(), "'Import ca (p12)' tab button should be active"
|
||||||
|
|
||||||
|
def get_certificate(self) -> dict:
|
||||||
|
""" Возвращает значания полей отображаемого сертификата"""
|
||||||
|
|
||||||
|
return self.view_certificate_form.get_certificate()
|
||||||
|
|
||||||
|
def get_identification_fields_values(self) -> dict:
|
||||||
|
"""Возвращает текущее значение полей блока 'Идентификация CA' формы для пересоздания сертификата.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict : Текущее значение полей блока 'Идентификация CA' формы для пересоздания сертификата.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.reissue_certificate_form.get_identification_fields_values()
|
||||||
|
|
||||||
|
def get_location_fields_values(self) -> dict:
|
||||||
|
"""Возвращает текущее значение полей блока 'Адрес / Местонахождение' формы для пересоздания сертификата.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict : Текущее значение полей блока блока 'Адрес / Местонахождение' формы для пересоздания сертификата.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.reissue_certificate_form.get_location_fields_values()
|
||||||
|
|
||||||
|
def get_password_field_value(self) -> str:
|
||||||
|
"""Возвращает текущее значение поля 'Пароль' формы импорта сертификата.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str : Текущее значение поля 'Пароль' формы импорта сертификата.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.import_certificate_form.get_password_field_value()
|
||||||
|
|
||||||
|
def export_certificate(self) -> str:
|
||||||
|
"""Нажатие кнопки 'Экспорт сертификата (CA)' в форме отображения сертификата и
|
||||||
|
скачивание текущего корневого сертификата.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str : Полный путь к скачанному файлу.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.view_certificate_form.export_certificate()
|
||||||
|
|
||||||
|
def input_identification_cert_name_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Имя Сертификата' блока 'Идентификация CA' формы для пересоздания сертификата"""
|
||||||
|
|
||||||
|
self.reissue_certificate_form.input_identification_cert_name_field(value)
|
||||||
|
|
||||||
|
def input_identification_organization_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Организация' блока 'Идентификация CA' формы для пересоздания сертификата"""
|
||||||
|
|
||||||
|
self.reissue_certificate_form.input_identification_organization_field(value)
|
||||||
|
|
||||||
|
def input_identification_org_unit_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Подразделение' блока 'Идентификация CA' формы для пересоздания сертификата"""
|
||||||
|
|
||||||
|
self.reissue_certificate_form.input_identification_org_unit_field(value)
|
||||||
|
|
||||||
|
def input_location_country_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Страна' блока 'Адрес / Местонахождение' формы для пересоздания сертификата"""
|
||||||
|
|
||||||
|
self.reissue_certificate_form.input_location_country_field(value)
|
||||||
|
|
||||||
|
def input_location_state_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Регион / Область' блока 'Адрес / Местонахождение' формы для пересоздания сертификата"""
|
||||||
|
|
||||||
|
self.reissue_certificate_form.input_location_state_field(value)
|
||||||
|
|
||||||
|
def input_location_city_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Город' блока 'Адрес / Местонахождение' формы для пересоздания сертификата"""
|
||||||
|
|
||||||
|
self.reissue_certificate_form.input_location_city_field(value)
|
||||||
|
|
||||||
|
def input_password_field(self, value: str) -> None:
|
||||||
|
"""Заполнение поля 'Пароль' формы импорта сертификата"""
|
||||||
|
|
||||||
|
self.import_certificate_form.input_password_field(value)
|
||||||
|
|
||||||
|
# Проверки:
|
||||||
|
def check_content(self):
|
||||||
|
"""Проверяет наличие и корректность всех элементов страницы."""
|
||||||
|
|
||||||
|
self.toolbar_main.check_toolbar_presence(['Сертификаты'])
|
||||||
|
self.toolbar_secondary.check_toolbar_presence(['Центр сертификации (CA)',
|
||||||
|
'Управление корневым сертификатом системы'])
|
||||||
|
|
||||||
|
self.click_certificate_tab_button()
|
||||||
|
self.view_certificate_form.check_content()
|
||||||
|
|
||||||
|
self.click_reissue_tab_button()
|
||||||
|
self.reissue_certificate_form.check_content()
|
||||||
|
|
||||||
|
self.click_import_tab_button()
|
||||||
|
self.import_certificate_form.check_content()
|
||||||
|
|
||||||
|
def check_alert(self, alert_type: str, alert_text: str) -> None:
|
||||||
|
"""Проверяет наличие alert заданного типа и текста."""
|
||||||
|
|
||||||
|
actual_alert_type = self.alert.get_alert_type()
|
||||||
|
assert actual_alert_type == alert_type, f"Got unexpected alert type {actual_alert_type}"
|
||||||
|
|
||||||
|
self.alert.check_alert_presence(alert_text)
|
||||||
|
self.alert.check_alert_absence(alert_text)
|
||||||
|
|
||||||
|
def is_import_button_disabled(self) -> bool:
|
||||||
|
"""Проверяет доступность кнопки импорта сертификата."""
|
||||||
|
|
||||||
|
return self.import_certificate_form.is_import_button_disabled()
|
||||||
|
|
||||||
|
def is_reissue_button_disabled(self) -> bool:
|
||||||
|
"""Проверяет доступность кнопки перевыпуска сертификата."""
|
||||||
|
|
||||||
|
return self.reissue_certificate_form.is_reissue_button_disabled()
|
||||||
|
|
@ -414,8 +414,8 @@ class EmailNotificationsSettingsTab(BasePage):
|
||||||
|
|
||||||
if name == "checkbox_activate":
|
if name == "checkbox_activate":
|
||||||
is_activate_checked = item.is_checked()
|
is_activate_checked = item.is_checked()
|
||||||
assert not is_activate_checked, (
|
assert is_activate_checked, (
|
||||||
"Checkbox 'Активировать' should not be checked by default"
|
"Checkbox 'Активировать' should be checked"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_tls_settings_content(self):
|
def _check_tls_settings_content(self):
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,19 @@ class LDAPAuthSettingsTab(BasePage):
|
||||||
if name == "password_hidden_icon":
|
if name == "password_hidden_icon":
|
||||||
is_hidden_state = item.is_password_hidden()
|
is_hidden_state = item.is_password_hidden()
|
||||||
assert is_hidden_state, "Password hidden icon should be in hidden state"
|
assert is_hidden_state, "Password hidden icon should be in hidden state"
|
||||||
|
hidden_password = self.get_password_setting_value()
|
||||||
|
|
||||||
|
self.click_password_hidden_icon()
|
||||||
|
is_hidden_state = item.is_password_hidden()
|
||||||
|
assert not is_hidden_state, "Password hidden icon should be in viewed state"
|
||||||
|
viewed_password = self.get_password_setting_value()
|
||||||
|
|
||||||
|
assert len(hidden_password) == len(viewed_password), \
|
||||||
|
"The lengths of hidden and viewed passwords should be equal"
|
||||||
|
|
||||||
|
self.click_password_hidden_icon()
|
||||||
|
is_hidden_state = item.is_password_hidden()
|
||||||
|
assert is_hidden_state, "Password hidden icon should be in hidden state"
|
||||||
|
|
||||||
def should_be_toolbar(self) -> None:
|
def should_be_toolbar(self) -> None:
|
||||||
"""Проверяет наличие тулбара страницы, наличие и функциональность кнопок тулбара.
|
"""Проверяет наличие тулбара страницы, наличие и функциональность кнопок тулбара.
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,13 @@ class LicenseTab(BasePage):
|
||||||
def scroll_json_container_up(self) -> None:
|
def scroll_json_container_up(self) -> None:
|
||||||
"""Прокручивает JSON-контейнер вверх."""
|
"""Прокручивает JSON-контейнер вверх."""
|
||||||
|
|
||||||
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER).first
|
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER)
|
||||||
self.json_container.scroll_up(loc)
|
self.json_container.scroll_up(loc)
|
||||||
|
|
||||||
def scroll_json_container_down(self) -> None:
|
def scroll_json_container_down(self) -> None:
|
||||||
"""Прокручивает JSON-контейнер вниз."""
|
"""Прокручивает JSON-контейнер вниз."""
|
||||||
|
|
||||||
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER).first
|
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER)
|
||||||
self.json_container.scroll_down(loc)
|
self.json_container.scroll_down(loc)
|
||||||
|
|
||||||
# Проверки:
|
# Проверки:
|
||||||
|
|
@ -77,7 +77,7 @@ class LicenseTab(BasePage):
|
||||||
bool: Доступность прокрутки
|
bool: Доступность прокрутки
|
||||||
"""
|
"""
|
||||||
|
|
||||||
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER).first
|
loc = self.page.locator(JsonContainerLocators.SCROLL_CONTAINER)
|
||||||
return self.json_container.is_scrollable_vertically(loc)
|
return self.json_container.is_scrollable_vertically(loc)
|
||||||
|
|
||||||
def check_content(self) -> None:
|
def check_content(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ class MainPage(BasePage):
|
||||||
item_name
|
item_name
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_navigation_panel_item_visibility(self, item_name: str) -> None:
|
def check_navigation_panel_item_visibility(self, item_name: str, parent=None) -> None:
|
||||||
"""Проверяет видимость элемента в панели навигации.
|
"""Проверяет видимость элемента в панели навигации.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -214,7 +214,7 @@ class MainPage(BasePage):
|
||||||
|
|
||||||
self.navigation_panel.check_item_visibility(
|
self.navigation_panel.check_item_visibility(
|
||||||
NavigationPanelLocators.PANEL_MAIN,
|
NavigationPanelLocators.PANEL_MAIN,
|
||||||
item_name
|
item_name, parent
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_subpanel_item_state(self, item_name: str, parent=None) -> str|None:
|
def check_subpanel_item_state(self, item_name: str, parent=None) -> str|None:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from locators.settings_form_locators import SettingsFormLocators
|
||||||
from elements.text_input_element import TextInput
|
from elements.text_input_element import TextInput
|
||||||
from components.alert_component import AlertComponent
|
from components.alert_component import AlertComponent
|
||||||
from components_derived.settings_form_component import SettingsFormComponent
|
from components_derived.settings_form_component import SettingsFormComponent
|
||||||
from components_derived.interactive_dropdown_list import InteractiveDropdownList
|
from components.checkbox_group_component import CheckboxGroupComponent # Изменен импорт
|
||||||
from pages.base_page import BasePage
|
from pages.base_page import BasePage
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -40,13 +40,13 @@ class PushNotificationsSettingsTab(BasePage):
|
||||||
message_setting_input = TextInput(page, loc_message_input, "message_setting_input")
|
message_setting_input = TextInput(page, loc_message_input, "message_setting_input")
|
||||||
self.settings_form.add_content_item("message_setting_input", message_setting_input)
|
self.settings_form.add_content_item("message_setting_input", message_setting_input)
|
||||||
|
|
||||||
|
|
||||||
loc = self.input_fields_locators.get("Пользователи")
|
loc = self.input_fields_locators.get("Пользователи")
|
||||||
users_setting_input = TextInput(page,
|
users_setting_input = TextInput(page,
|
||||||
loc.get_by_role("combobox"),
|
loc.get_by_role("combobox"),
|
||||||
"users_setting_input")
|
"users_setting_input")
|
||||||
self.settings_form.add_content_item("users_setting_input", users_setting_input)
|
self.settings_form.add_content_item("users_setting_input", users_setting_input)
|
||||||
self.settings_form.add_content_item("users_list", InteractiveDropdownList(page))
|
# Используем новый компонент CheckboxGroupComponent
|
||||||
|
self.settings_form.add_content_item("users_checkbox_group", CheckboxGroupComponent(page))
|
||||||
|
|
||||||
self.settings_form.add_tooltip_button(page.locator(SettingsFormLocators.PUSH_NOTIFICATIONS_BUTTON_SUBMIT),
|
self.settings_form.add_tooltip_button(page.locator(SettingsFormLocators.PUSH_NOTIFICATIONS_BUTTON_SUBMIT),
|
||||||
"submit_button")
|
"submit_button")
|
||||||
|
|
@ -104,10 +104,10 @@ class PushNotificationsSettingsTab(BasePage):
|
||||||
assert len(users) != 0, "Users list should not be empty"
|
assert len(users) != 0, "Users list should not be empty"
|
||||||
|
|
||||||
self.settings_form.get_content_item("users_setting_input").click()
|
self.settings_form.get_content_item("users_setting_input").click()
|
||||||
users_list = self.settings_form.get_content_item("users_list")
|
users_checkbox_group = self.settings_form.get_content_item("users_checkbox_group")
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
users_list.deselect_item_with_text(user)
|
users_checkbox_group.uncheck_by_text(user)
|
||||||
|
|
||||||
# Закрываем выпадающий список (кликаем вне его)
|
# Закрываем выпадающий список (кликаем вне его)
|
||||||
self.page.mouse.click(10, 10)
|
self.page.mouse.click(10, 10)
|
||||||
|
|
@ -118,10 +118,10 @@ class PushNotificationsSettingsTab(BasePage):
|
||||||
assert len(users) != 0, "Users list should not be empty"
|
assert len(users) != 0, "Users list should not be empty"
|
||||||
|
|
||||||
self.settings_form.get_content_item("users_setting_input").click()
|
self.settings_form.get_content_item("users_setting_input").click()
|
||||||
users_list = self.settings_form.get_content_item("users_list")
|
users_checkbox_group = self.settings_form.get_content_item("users_checkbox_group")
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
users_list.select_item_with_text(user)
|
users_checkbox_group.check_by_text(user)
|
||||||
|
|
||||||
# Закрываем выпадающий список (кликаем вне его)
|
# Закрываем выпадающий список (кликаем вне его)
|
||||||
self.page.mouse.click(10, 10)
|
self.page.mouse.click(10, 10)
|
||||||
|
|
@ -142,10 +142,10 @@ class PushNotificationsSettingsTab(BasePage):
|
||||||
f"Misscomparison input field names: Expected {expected_input_field_names}, Actual {actual_input_field_names}"
|
f"Misscomparison input field names: Expected {expected_input_field_names}, Actual {actual_input_field_names}"
|
||||||
|
|
||||||
for name in self.settings_form.content_items.keys():
|
for name in self.settings_form.content_items.keys():
|
||||||
if name == "users_list":
|
if name == "users_checkbox_group":
|
||||||
self.settings_form.get_content_item("users_setting_input").click()
|
self.settings_form.get_content_item("users_setting_input").click()
|
||||||
users_list = self.settings_form.get_content_item(name)
|
users_checkbox_group = self.settings_form.get_content_item(name)
|
||||||
selected_users = users_list.get_selected_items(SettingsFormLocators.DROPDOWN_LIST)
|
selected_users = users_checkbox_group.get_checked_items(SettingsFormLocators.DROPDOWN_LIST)
|
||||||
assert len(selected_users) == 0, "There should be no selected users"
|
assert len(selected_users) == 0, "There should be no selected users"
|
||||||
else:
|
else:
|
||||||
item = self.settings_form.get_content_item(name)
|
item = self.settings_form.get_content_item(name)
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,6 @@ class SessionSettingsTab(BasePage):
|
||||||
field.input_value(value)
|
field.input_value(value)
|
||||||
|
|
||||||
# temporararily
|
# temporararily
|
||||||
self.click_cancel_button()
|
|
||||||
|
|
||||||
# self.click_save_button()
|
# self.click_save_button()
|
||||||
|
|
||||||
# alert_type = self.alert.get_alert_type()
|
# alert_type = self.alert.get_alert_type()
|
||||||
|
|
@ -183,16 +181,14 @@ class SessionSettingsTab(BasePage):
|
||||||
"""Скроллинг вниз формы настроек времени жизни сессии.
|
"""Скроллинг вниз формы настроек времени жизни сессии.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
locator = self.page.locator(SettingsFormLocators.SETTTINGS_FORM_SCROLL_CONTAINER).filter(
|
locator = self.page.locator(SettingsFormLocators.SETTINGS_FORM_INPUT_FORM_CONTAINER)
|
||||||
has_text="Время жизни сеанса")
|
|
||||||
self.settings_form.scroll_down(locator)
|
self.settings_form.scroll_down(locator)
|
||||||
|
|
||||||
def scroll_up(self) -> None:
|
def scroll_up(self) -> None:
|
||||||
"""Скроллинг вверх формы настроек времени жизни сессии.
|
"""Скроллинг вверх формы настроек времени жизни сессии.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
locator = self.page.locator(SettingsFormLocators.SETTTINGS_FORM_SCROLL_CONTAINER).filter(
|
locator = self.page.locator(SettingsFormLocators.SETTINGS_FORM_INPUT_FORM_CONTAINER)
|
||||||
has_text="Время жизни сеанса")
|
|
||||||
self.settings_form.scroll_up(locator)
|
self.settings_form.scroll_up(locator)
|
||||||
|
|
||||||
# Проверки:
|
# Проверки:
|
||||||
|
|
@ -227,8 +223,7 @@ class SessionSettingsTab(BasePage):
|
||||||
"""Проверка возможности вертикального скроллинга формы настроек времени жизни сессии.
|
"""Проверка возможности вертикального скроллинга формы настроек времени жизни сессии.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
locator = self.page.locator(SettingsFormLocators.SETTTINGS_FORM_SCROLL_CONTAINER).filter(
|
locator = self.page.locator(SettingsFormLocators.SETTINGS_FORM_INPUT_FORM_CONTAINER)
|
||||||
has_text="Время жизни сеанса")
|
|
||||||
return self.settings_form.check_vertical_scrolling(locator)
|
return self.settings_form.check_vertical_scrolling(locator)
|
||||||
|
|
||||||
def should_be_toolbar(self) -> None:
|
def should_be_toolbar(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,18 @@ class UsersTab(BasePage):
|
||||||
assert False, f"Modal window with title '{title}' not found"
|
assert False, f"Modal window with title '{title}' not found"
|
||||||
return modal_window
|
return modal_window
|
||||||
|
|
||||||
|
def get_rows_count(self) -> int:
|
||||||
|
"""Возвращает количество строк в таблице пользователей (без заголовка).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Количество строк с данными.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: Если таблица пуста.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.users_table.get_rows_count(TableLocators.TABLE_WORK_AREA)
|
||||||
|
|
||||||
def open_add_user_window(self) -> None:
|
def open_add_user_window(self) -> None:
|
||||||
"""Открывает окно добавления пользователя.
|
"""Открывает окно добавления пользователя.
|
||||||
|
|
||||||
|
|
@ -396,6 +408,21 @@ class UsersTab(BasePage):
|
||||||
if verify:
|
if verify:
|
||||||
self.verify_users_table_content(table_content)
|
self.verify_users_table_content(table_content)
|
||||||
|
|
||||||
|
def check_users_table_row_highlighting(self, row_index: int) -> None:
|
||||||
|
"""Проверяет выделение указанной строки таблицы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row_index: Индекс проверяемой строки.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: Если строка не выделена.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.users_table.check_row_highlighting(
|
||||||
|
TableLocators.TABLE_WORK_AREA,
|
||||||
|
row_index
|
||||||
|
)
|
||||||
|
|
||||||
def should_be_toolbar(self) -> None:
|
def should_be_toolbar(self) -> None:
|
||||||
"""Проверяет наличие тулбара.
|
"""Проверяет наличие тулбара.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
pytest -s -v --s="{'width': 300, 'height': 420}" test_navigation_panel.py
|
pytest -s -v --s="{'width': 300, 'height': 420}" test_navigation_panel.py
|
||||||
pytest -s -v --s="{'width': 1500, 'height': 420}" test_services_table.py
|
pytest -s -v --s="{'width': 1500, 'height': 420}" test_services_table.py
|
||||||
pytest -s -v --s="{'width': 300, 'height': 420}" test_json_container.py
|
pytest -s -v --s="{'width': 300, 'height': 600}" test_json_container.py
|
||||||
pytest -s -v --s="{'width': 300, 'height': 420}" test_user_modal_window.py
|
pytest -s -v --s="{'width': 300, 'height': 420}" test_user_modal_window.py
|
||||||
pytest -s -v --s="{'width': 800, 'height': 200}" test_session_settings.py
|
pytest -s -v --s="{'width': 800, 'height': 200}" test_session_settings.py
|
||||||
|
|
@ -54,10 +54,10 @@ class TestNavigationPanel:
|
||||||
# Действия:
|
# Действия:
|
||||||
# Прокручиваем вверх и проверяем видимость элемента
|
# Прокручиваем вверх и проверяем видимость элемента
|
||||||
mp.scroll_navigation_panel_up()
|
mp.scroll_navigation_panel_up()
|
||||||
mp.check_navigation_panel_item_visibility("Панель приборов")
|
mp.check_navigation_panel_item_visibility("Панели")
|
||||||
mp.wait_for_timeout(3000)
|
mp.wait_for_timeout(3000)
|
||||||
|
|
||||||
# Прокручиваем вниз и проверяем видимость элемента Настройки/ZTP/Шаблоны
|
# Прокручиваем вниз и проверяем видимость элемента Настройки/ZTP/Шаблоны
|
||||||
mp.scroll_navigation_panel_down()
|
mp.scroll_navigation_panel_down()
|
||||||
mp.check_navigation_panel_item_visibility("Шаблоны_2")
|
mp.check_navigation_panel_item_visibility("Шаблоны")
|
||||||
mp.wait_for_timeout(2000)
|
mp.wait_for_timeout(2000)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class TestSessionSettingsForm:
|
||||||
mp.click_subpanel_item("Настройки", parent="Сеансы")
|
mp.click_subpanel_item("Настройки", parent="Сеансы")
|
||||||
|
|
||||||
def test_scrolling(self, browser: Page) -> None:
|
def test_scrolling(self, browser: Page) -> None:
|
||||||
"""Проверяет прокрутку таблицы статусов сервисов.
|
"""Проверяет прокрутку формы редактирования настроек.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
browser: Экземпляр страницы Playwright.
|
browser: Экземпляр страницы Playwright.
|
||||||
|
|
@ -55,5 +55,5 @@ class TestSessionSettingsForm:
|
||||||
sst.wait_for_timeout(3000)
|
sst.wait_for_timeout(3000)
|
||||||
|
|
||||||
sst.scroll_up()
|
sst.scroll_up()
|
||||||
sst.should_be_form_toolbar()
|
sst.get_field_by_name('administrator').check_visibility("Text 'Администратор' should be visible")
|
||||||
sst.wait_for_timeout(2000)
|
sst.wait_for_timeout(2000)
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ class TestUsersModalWindow:
|
||||||
|
|
||||||
ut = UsersTab(browser)
|
ut = UsersTab(browser)
|
||||||
ut.open_add_user_window()
|
ut.open_add_user_window()
|
||||||
modal_window = ut.get_modal_window("add_local_user")
|
modal_window = ut.get_modal_window("add_user")
|
||||||
|
|
||||||
is_scrollable_vertically = modal_window.check_window_vertical_scrolling()
|
is_scrollable_vertically = modal_window.check_window_vertical_scrolling()
|
||||||
assert is_scrollable_vertically, "Should be vertical scrolling"
|
assert is_scrollable_vertically, "Should be vertical scrolling"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class TestDateInputComponent:
|
||||||
# @pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
def test_date_input_content(self, browser: Page) -> None:
|
def test_date_input_content(self, browser: Page) -> None:
|
||||||
"""Проверяет содержимое компонента ввода даты.
|
"""Проверяет содержимое компонента ввода даты.
|
||||||
put
|
|
||||||
Args:
|
Args:
|
||||||
browser: Экземпляр страницы Playwright.
|
browser: Экземпляр страницы Playwright.
|
||||||
"""
|
"""
|
||||||
|
|
@ -61,7 +61,8 @@ put
|
||||||
date_input.click_switch_mode_button()
|
date_input.click_switch_mode_button()
|
||||||
browser.wait_for_timeout(500)
|
browser.wait_for_timeout(500)
|
||||||
|
|
||||||
if date_input.is_text_input_mode():
|
# Temporarily due to error in UI
|
||||||
|
if not date_input.is_text_input_mode():
|
||||||
# выбираем 15 января 2024, проверяем результат
|
# выбираем 15 января 2024, проверяем результат
|
||||||
expected_date = "15.01.2024"
|
expected_date = "15.01.2024"
|
||||||
date_input.input_date(expected_date)
|
date_input.input_date(expected_date)
|
||||||
|
|
@ -95,7 +96,8 @@ put
|
||||||
date_input.click_switch_mode_button()
|
date_input.click_switch_mode_button()
|
||||||
browser.wait_for_timeout(500)
|
browser.wait_for_timeout(500)
|
||||||
|
|
||||||
if date_input.is_text_input_mode():
|
# Temporarily due to error in UI
|
||||||
|
if not date_input.is_text_input_mode():
|
||||||
try:
|
try:
|
||||||
date_input.input_date("1.01.2020")
|
date_input.input_date("1.01.2020")
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
|
|
@ -169,7 +171,8 @@ put
|
||||||
date_input.click_switch_mode_button()
|
date_input.click_switch_mode_button()
|
||||||
browser.wait_for_timeout(500)
|
browser.wait_for_timeout(500)
|
||||||
|
|
||||||
if date_input.is_text_input_mode():
|
# Temporarily due to error in UI
|
||||||
|
if not date_input.is_text_input_mode():
|
||||||
# выбираем 15 января 2024 10:11, проверяем результат
|
# выбираем 15 января 2024 10:11, проверяем результат
|
||||||
date_input.input_date("15.01.2024")
|
date_input.input_date("15.01.2024")
|
||||||
browser.wait_for_timeout(300)
|
browser.wait_for_timeout(300)
|
||||||
|
|
@ -208,7 +211,8 @@ put
|
||||||
date_input.click_switch_mode_button()
|
date_input.click_switch_mode_button()
|
||||||
browser.wait_for_timeout(500)
|
browser.wait_for_timeout(500)
|
||||||
|
|
||||||
if date_input.is_text_input_mode():
|
# Temporarily due to error in UI
|
||||||
|
if not date_input.is_text_input_mode():
|
||||||
assert False, "Should be date input mode by date picker"
|
assert False, "Should be date input mode by date picker"
|
||||||
else:
|
else:
|
||||||
# выбираем 16 января 2024 08:11, проверяем результат
|
# выбираем 16 января 2024 08:11, проверяем результат
|
||||||
|
|
@ -245,26 +249,27 @@ put
|
||||||
date_input.click_switch_mode_button()
|
date_input.click_switch_mode_button()
|
||||||
browser.wait_for_timeout(500)
|
browser.wait_for_timeout(500)
|
||||||
|
|
||||||
if date_input.is_text_input_mode():
|
# Temporarily due to error in UI
|
||||||
|
if not date_input.is_text_input_mode():
|
||||||
# выбираем 15 января 2024 10:11, проверяем результат
|
# выбираем 15 января 2024 10:11, проверяем результат
|
||||||
date_input.input_date("15.01.2024")
|
date_input.input_date("15.01.2024")
|
||||||
browser.wait_for_timeout(300)
|
browser.wait_for_timeout(300)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_input.time_date("1:01")
|
date_input.input_time("1:01")
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
actual_err = f"{e}"
|
actual_err = f"{e}"
|
||||||
expected_err = "Incorrect time format: should be 'hh:mm'"
|
expected_err = "Incorrect time format: should be 'hh:mm'"
|
||||||
assert expected_err == actual_err, \
|
assert expected_err == actual_err, \
|
||||||
f"Expected error message: '{expected_err}' is nor equal actual error message: '{actual_err}'"
|
f"Expected error message: '{expected_err}' is not equal actual error message: '{actual_err}'"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_input.input_date("o2.01")
|
date_input.input_date("o2.01.2024")
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
actual_err = f"{e}"
|
actual_err = f"{e}"
|
||||||
expected_err = "Incorrect hours value o2 for selection"
|
expected_err = "Incorrect day value o2 for selection"
|
||||||
assert expected_err == actual_err, \
|
assert expected_err == actual_err, \
|
||||||
f"Expected error message: '{expected_err}' is nor equal actual error message: '{actual_err}'"
|
f"Expected error message: '{expected_err}' is not equal actual error message: '{actual_err}'"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_input.input_time("01:1")
|
date_input.input_time("01:1")
|
||||||
|
|
@ -272,14 +277,14 @@ put
|
||||||
actual_err = f"{e}"
|
actual_err = f"{e}"
|
||||||
expected_err = "Incorrect time format: should be 'hh:mm'"
|
expected_err = "Incorrect time format: should be 'hh:mm'"
|
||||||
assert expected_err == actual_err, \
|
assert expected_err == actual_err, \
|
||||||
f"Expected error message: '{expected_err}' is nor equal actual error message: '{actual_err}'"
|
f"Expected error message: '{expected_err}' is not equal actual error message: '{actual_err}'"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_input.input_date("01:o1")
|
date_input.input_date("01.o1.2024")
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
actual_err = f"{e}"
|
actual_err = f"{e}"
|
||||||
expected_err = "Incorrect minutes value o1 for selection"
|
expected_err = "Incorrect month value o1 for selection"
|
||||||
assert expected_err == actual_err, \
|
assert expected_err == actual_err, \
|
||||||
f"Expected error message: '{expected_err}' is nor equal actual error message: '{actual_err}'"
|
f"Expected error message: '{expected_err}' is not equal actual error message: '{actual_err}'"
|
||||||
else:
|
else:
|
||||||
assert False, "Should be text date input mode"
|
assert False, "Should be text date input mode"
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,10 @@ class TestDatePickerComponent:
|
||||||
browser.wait_for_timeout(300)
|
browser.wait_for_timeout(300)
|
||||||
|
|
||||||
month_num = months.index(current_month) + 1
|
month_num = months.index(current_month) + 1
|
||||||
expected_date = f"15.{month_num}.{current_year}"
|
if month_num < 10:
|
||||||
|
expected_date = f"15.{month_num:02d}.{current_year}"
|
||||||
|
else:
|
||||||
|
expected_date = f"15.{month_num}.{current_year}"
|
||||||
actual_date = date_input.get_date_field_value()
|
actual_date = date_input.get_date_field_value()
|
||||||
assert expected_date == actual_date, \
|
assert expected_date == actual_date, \
|
||||||
f"Expected date {expected_date} is not equal actual date {actual_date}"
|
f"Expected date {expected_date} is not equal actual date {actual_date}"
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class TestCreateRack:
|
||||||
|
|
||||||
# Переходим к Объектам
|
# Переходим к Объектам
|
||||||
self.main_page.click_main_navigation_panel_item("Объекты")
|
self.main_page.click_main_navigation_panel_item("Объекты")
|
||||||
self.main_page.wait_for_timeout(1000)
|
self.main_page.wait_for_timeout(2000)
|
||||||
self.main_page.click_main_navigation_panel_item("test-zone")
|
self.main_page.click_main_navigation_panel_item("test-zone")
|
||||||
|
|
||||||
# Создаем экземпляр страницы локации
|
# Создаем экземпляр страницы локации
|
||||||
|
|
@ -121,8 +121,8 @@ class TestCreateRack:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
# Ждем появления alert с текстом
|
# Ждем появления alert с текстом
|
||||||
expected_alert_text = f"Элемент {rack_data.name} создан"
|
expected_alert_text = f"Успешно создано"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ class TestCreateRack:
|
||||||
|
|
||||||
# Проверяем уведомление об успешном удалении
|
# Проверяем уведомление об успешном удалении
|
||||||
expected_alert_text = "Успешно удалено"
|
expected_alert_text = "Успешно удалено"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -213,6 +213,8 @@ class TestCreateRack:
|
||||||
def test_create_rack_content(self, browser: Page) -> None:
|
def test_create_rack_content(self, browser: Page) -> None:
|
||||||
"""Тест проверки содержимого формы создания стойки."""
|
"""Тест проверки содержимого формы создания стойки."""
|
||||||
|
|
||||||
|
self.main_page.wait_for_timeout(7000)
|
||||||
|
|
||||||
# Проверяем что кнопка "Создать" доступна
|
# Проверяем что кнопка "Создать" доступна
|
||||||
self.location_page.should_be_toolbar_buttons()
|
self.location_page.should_be_toolbar_buttons()
|
||||||
|
|
||||||
|
|
@ -331,7 +333,7 @@ class TestCreateRack:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
expected_alert_text = f"Имя {rack_name} уже используется"
|
expected_alert_text = f"Имя {rack_name} уже используется"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -345,13 +347,14 @@ class TestCreateRack:
|
||||||
|
|
||||||
logger.debug("System prevented creating rack with duplicate name")
|
logger.debug("System prevented creating rack with duplicate name")
|
||||||
|
|
||||||
|
@pytest.mark.develop
|
||||||
def test_required_fields_validation(self, browser: Page, cleanup_racks) -> None:
|
def test_required_fields_validation(self, browser: Page, cleanup_racks) -> None:
|
||||||
"""Тест проверки обязательных полей при создании стойки."""
|
"""Тест проверки обязательных полей при создании стойки."""
|
||||||
logger.debug("Starting required fields validation test")
|
logger.debug("Starting required fields validation test")
|
||||||
|
|
||||||
expected_alert_text_height = "поле Высота в юнитах должно быть заполнено"
|
expected_alert_text_height = "поле Высота в юнитах должно быть заполнено"
|
||||||
expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено"
|
expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено"
|
||||||
expected_alert_text_name = "Поле Имя должно быть установлено"
|
expected_alert_text_name = "Поле Имя должно быть заполнено"
|
||||||
|
|
||||||
self.main_page.click_main_navigation_panel_item("test-zone")
|
self.main_page.click_main_navigation_panel_item("test-zone")
|
||||||
|
|
||||||
|
|
@ -366,29 +369,31 @@ class TestCreateRack:
|
||||||
|
|
||||||
rack_form = CreateRackForm(browser)
|
rack_form = CreateRackForm(browser)
|
||||||
|
|
||||||
# ========== Тест 1: Обязательные поля высота и глубина пустые ==========
|
# ========== Тест 1: Обязательные поля имя, высота и глубина пустые ==========
|
||||||
logger.debug("Test 1: Both required fields (height, depth) are empty")
|
logger.debug("Test 1: Both required fields (height, depth) are empty")
|
||||||
|
|
||||||
# Очищаем поля высоты и глубины перед заполнением
|
# Очищаем поля имя, высоты и глубины перед заполнением
|
||||||
|
rack_form.clear_field("Имя")
|
||||||
rack_form.clear_field("Высота в юнитах")
|
rack_form.clear_field("Высота в юнитах")
|
||||||
rack_form.clear_field("Глубина (мм)")
|
rack_form.clear_field("Глубина (мм)")
|
||||||
|
|
||||||
test_data_1 = CreateRackData(
|
test_data_1 = CreateRackData(
|
||||||
name=self.TEST_RACK_NAME,
|
name="",
|
||||||
usize="",
|
usize="",
|
||||||
depth=""
|
depth=""
|
||||||
)
|
)
|
||||||
|
|
||||||
rack_form.fill_rack_data(test_data_1)
|
rack_form.fill_rack_data(test_data_1)
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
self.create_child_frame.wait_for_timeout(500)
|
|
||||||
|
|
||||||
# Проверяем alert для высоты, глубины
|
# Проверяем alert для имени, высоты, глубины
|
||||||
self.alert.check_alert_presence(expected_alert_text_height, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_name, timeout=7000)
|
||||||
self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_height, timeout=7000)
|
||||||
|
self.alert.check_alert_presence(expected_alert_text_depth, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для высоты, глубины
|
# Проверяем, закрылся ли автоматически alert для имени, высоты, глубины
|
||||||
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_name, timeout=7000)
|
||||||
|
self.alert.check_alert_absence(expected_alert_text_height, timeout=500)
|
||||||
self.alert.check_alert_absence(expected_alert_text_depth, timeout=500)
|
self.alert.check_alert_absence(expected_alert_text_depth, timeout=500)
|
||||||
|
|
||||||
# Проверяем подсветку полей
|
# Проверяем подсветку полей
|
||||||
|
|
@ -413,7 +418,7 @@ class TestCreateRack:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
# Проверяем alert для глубины
|
# Проверяем alert для глубины
|
||||||
self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_depth, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для глубины
|
# Проверяем, закрылся ли автоматически alert для глубины
|
||||||
self.alert.check_alert_absence(expected_alert_text_depth, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_depth, timeout=7000)
|
||||||
|
|
@ -439,7 +444,7 @@ class TestCreateRack:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
# Проверяем alert для высоты
|
# Проверяем alert для высоты
|
||||||
self.alert.check_alert_presence(expected_alert_text_height, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_height, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для высоты
|
# Проверяем, закрылся ли автоматически alert для высоты
|
||||||
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
|
||||||
|
|
@ -463,12 +468,14 @@ class TestCreateRack:
|
||||||
|
|
||||||
rack_form.fill_rack_data(test_data_4)
|
rack_form.fill_rack_data(test_data_4)
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
self.create_child_frame.wait_for_timeout(500)
|
|
||||||
|
|
||||||
# Проверяем alert для имени
|
# Проверяем alert для имени
|
||||||
self.alert.check_alert_presence(expected_alert_text_name, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_name, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для высоты
|
# Проверяем, закрылся ли автоматически alert для высоты
|
||||||
self.alert.check_alert_absence(expected_alert_text_name, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_name, timeout=7000)
|
||||||
|
|
||||||
logger.debug("Test 4 completed: System correctly validates empty name field")
|
logger.debug("Test 4 completed: System correctly validates empty name field")
|
||||||
|
|
||||||
|
# ========== Тест 5: Поле "Имя" не более 35 знаков ==========
|
||||||
|
# разработчик должен ограничить длину имени - не более 35 знаков
|
||||||
|
|
@ -197,8 +197,8 @@ class TestRackEdit:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
# Ждем появления alert с текстом
|
# Ждем появления alert с текстом
|
||||||
expected_alert_text = f"Элемент {rack_data.name} создан"
|
expected_alert_text = f"Успешно создано"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -247,7 +247,7 @@ class TestRackEdit:
|
||||||
|
|
||||||
# Проверяем уведомление об успешном удалении
|
# Проверяем уведомление об успешном удалении
|
||||||
expected_alert_text = "Успешно удалено"
|
expected_alert_text = "Успешно удалено"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -304,8 +304,8 @@ class TestRackEdit:
|
||||||
rack_edit.click_done_button()
|
rack_edit.click_done_button()
|
||||||
|
|
||||||
# Проверяем уведомление об успешном обновлении
|
# Проверяем уведомление об успешном обновлении
|
||||||
expected_alert_text = "Элемент успешно обновлён"
|
expected_alert_text = "Успешно обновлено"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -336,6 +336,54 @@ class TestRackEdit:
|
||||||
|
|
||||||
logger.debug("General info tab test completed successfully")
|
logger.debug("General info tab test completed successfully")
|
||||||
|
|
||||||
|
def test_required_field_name_validation(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки обязательного поля ИМЯ при создании стойки."""
|
||||||
|
|
||||||
|
logger.debug(f"Starting required field name validation test for rack: {self.RACK_NAME}")
|
||||||
|
|
||||||
|
rack_page = RackPage(browser)
|
||||||
|
|
||||||
|
# Переходим в режим редактирования
|
||||||
|
rack_page.click_edit_button()
|
||||||
|
|
||||||
|
# Создаем экземпляр EditRackMaker
|
||||||
|
rack_edit = EditRackMaker(browser, self.RACK_NAME)
|
||||||
|
|
||||||
|
# ========== Тест 1: Обязательное поле имя пустое ==========
|
||||||
|
# Очищаем поле имя
|
||||||
|
rack_edit.clear_field("Имя")
|
||||||
|
|
||||||
|
# Создаем тестовые данные для заполнения поля
|
||||||
|
test_data_1 = EditRackData(
|
||||||
|
name=""
|
||||||
|
)
|
||||||
|
|
||||||
|
rack_edit.fill_rack_data(test_data_1)
|
||||||
|
rack_edit.click_done_button()
|
||||||
|
|
||||||
|
# Проверяем уведомление об ошибочном обновлении
|
||||||
|
expected_alert_text = "поле ИМЯ должно быть заполнено, и не должно превышать 35 знаков"
|
||||||
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
# ========== Тест 2: Обязательное поле имя не должно превышать 35 знаков ==========
|
||||||
|
# Создаем тестовые данные для заполнения поля
|
||||||
|
test_data_2 = EditRackData(
|
||||||
|
name="_123456789_123456789_123456789_12345"
|
||||||
|
)
|
||||||
|
|
||||||
|
rack_edit.fill_rack_data(test_data_2)
|
||||||
|
rack_edit.click_done_button()
|
||||||
|
|
||||||
|
# Проверяем уведомление об ошибочном обновлении
|
||||||
|
expected_alert_text = "поле ИМЯ должно быть заполнено, и не должно превышать 35 знаков"
|
||||||
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
rack_edit.click_close_button()
|
||||||
|
|
||||||
|
logger.debug("Required field name validation test completed successfully")
|
||||||
|
|
||||||
def test_rack_image_tab(self, browser: Page) -> None:
|
def test_rack_image_tab(self, browser: Page) -> None:
|
||||||
"""Тест вкладки 'Изображение' стойки."""
|
"""Тест вкладки 'Изображение' стойки."""
|
||||||
|
|
||||||
|
|
@ -376,8 +424,8 @@ class TestRackEdit:
|
||||||
rack_edit.click_done_button()
|
rack_edit.click_done_button()
|
||||||
|
|
||||||
# Проверяем уведомление об успешном обновлении
|
# Проверяем уведомление об успешном обновлении
|
||||||
expected_alert_text = "Элемент успешно обновлён"
|
expected_alert_text = "Успешно обновлено"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -466,8 +514,8 @@ class TestRackEdit:
|
||||||
rack_edit.click_done_button()
|
rack_edit.click_done_button()
|
||||||
|
|
||||||
# Проверяем уведомление об успешном обновлении
|
# Проверяем уведомление об успешном обновлении
|
||||||
expected_alert_text = "Элемент успешно обновлён"
|
expected_alert_text = "Успешно обновлено"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,8 +194,8 @@ class TestRackManagement:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
# Ждем появления alert с текстом
|
# Ждем появления alert с текстом
|
||||||
expected_alert_text = f"Элемент {rack_data.name} создан"
|
expected_alert_text = f"Успешно создано"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -244,7 +244,7 @@ class TestRackManagement:
|
||||||
|
|
||||||
# Проверяем уведомление об успешном удалении
|
# Проверяем уведомление об успешном удалении
|
||||||
expected_alert_text = "Успешно удалено"
|
expected_alert_text = "Успешно удалено"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,25 +53,27 @@ class TestActionsEventsContainer:
|
||||||
# Получение количества строк в таблице Реальное время
|
# Получение количества строк в таблице Реальное время
|
||||||
rows_count = actions_events_container.get_events_table_rows_count()
|
rows_count = actions_events_container.get_events_table_rows_count()
|
||||||
|
|
||||||
# Проверка выделения строк
|
if rows_count != 0:
|
||||||
actions_events_container.check_events_table_row_highlighting(0)
|
# Проверка выделения строк
|
||||||
if rows_count > 1:
|
actions_events_container.check_events_table_row_highlighting(0)
|
||||||
actions_events_container.check_events_table_row_highlighting(rows_count - 1)
|
if rows_count > 1:
|
||||||
if rows_count > 3:
|
actions_events_container.check_events_table_row_highlighting(rows_count - 1)
|
||||||
actions_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
if rows_count > 3:
|
||||||
|
actions_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
actions_events_container.click_archive_button()
|
actions_events_container.click_archive_button()
|
||||||
# Получение количества строк в таблице Архив
|
# Получение количества строк в таблице Архив
|
||||||
rows_count = actions_events_container.get_events_table_rows_count()
|
rows_count = actions_events_container.get_events_table_rows_count()
|
||||||
|
|
||||||
# Проверка выделения строк
|
if rows_count != 0:
|
||||||
actions_events_container.check_events_table_row_highlighting(0)
|
# Проверка выделения строк
|
||||||
if rows_count > 1:
|
actions_events_container.check_events_table_row_highlighting(0)
|
||||||
actions_events_container.check_events_table_row_highlighting(rows_count - 1)
|
if rows_count > 1:
|
||||||
if rows_count > 3:
|
actions_events_container.check_events_table_row_highlighting(rows_count - 1)
|
||||||
actions_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
if rows_count > 3:
|
||||||
|
actions_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
@pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
def test_events_table_scrolling(self, browser: Page):
|
def test_events_table_scrolling(self, browser: Page):
|
||||||
"""Проверяет возможность скроллинга таблицы событий на примере таблицы Архив.
|
"""Проверяет возможность скроллинга таблицы событий на примере таблицы Архив.
|
||||||
|
|
||||||
|
|
@ -79,6 +81,8 @@ class TestActionsEventsContainer:
|
||||||
browser: Экземпляр страницы Playwright.
|
browser: Экземпляр страницы Playwright.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
rows_to_start_scrolling = 20
|
||||||
|
|
||||||
lp = LoginPage(browser)
|
lp = LoginPage(browser)
|
||||||
lp.do_login()
|
lp.do_login()
|
||||||
|
|
||||||
|
|
@ -88,58 +92,62 @@ class TestActionsEventsContainer:
|
||||||
actions_events_container = mp.click_events_panel_actions_tab()
|
actions_events_container = mp.click_events_panel_actions_tab()
|
||||||
actions_events_container.click_archive_button()
|
actions_events_container.click_archive_button()
|
||||||
|
|
||||||
events_panel_position = mp.get_events_panel_position()
|
rows_count = actions_events_container.get_events_table_rows_count()
|
||||||
|
if rows_count > rows_to_start_scrolling:
|
||||||
|
events_panel_position = mp.get_events_panel_position()
|
||||||
|
|
||||||
# Проверка, что панель с таблицей открыта
|
# Проверка, что панель с таблицей открыта
|
||||||
assert events_panel_position != "bottom", "Panel with actions events should be opened"
|
assert events_panel_position != "bottom", "Panel with actions events should be opened"
|
||||||
|
|
||||||
is_scrollable = actions_events_container.check_events_table_verticall_scrolling()
|
is_scrollable = actions_events_container.check_events_table_verticall_scrolling()
|
||||||
|
|
||||||
# Убеждаемся, что панель открыта наполовину и проверяем скроллинг
|
# Убеждаемся, что панель открыта наполовину и проверяем скроллинг
|
||||||
assert events_panel_position == "center",\
|
assert events_panel_position == "center",\
|
||||||
"Panel with actions events should be located on the main page center"
|
"Panel with actions events should be located on the main page center"
|
||||||
assert is_scrollable, "Actions events table should be scrollable"
|
assert is_scrollable, "Actions events table should be scrollable"
|
||||||
|
|
||||||
# Скроллинг вниз
|
# Скроллинг вниз
|
||||||
actions_events_container.scroll_events_table_down()
|
actions_events_container.scroll_events_table_down()
|
||||||
browser.wait_for_timeout(1000)
|
browser.wait_for_timeout(1000)
|
||||||
|
|
||||||
# Проверка видимости последней строки после прокрутки
|
# Проверка видимости последней строки после прокрутки
|
||||||
actions_events_container.check_events_table_last_row_visibility()
|
actions_events_container.check_events_table_last_row_visibility()
|
||||||
|
|
||||||
# Скроллинг вверх
|
# Скроллинг вверх
|
||||||
actions_events_container.scroll_events_table_up()
|
actions_events_container.scroll_events_table_up()
|
||||||
browser.wait_for_timeout(1000)
|
browser.wait_for_timeout(1000)
|
||||||
|
|
||||||
# Проверка видимости первой строки после прокрутки
|
# Проверка видимости первой строки после прокрутки
|
||||||
actions_events_container.check_events_table_first_row_visibility()
|
actions_events_container.check_events_table_first_row_visibility()
|
||||||
|
|
||||||
# Раскрываем панель полностью и проверяем скроллинг
|
# Раскрываем панель полностью и проверяем скроллинг
|
||||||
assert mp.check_expand_more_button(), \
|
assert mp.check_expand_more_button(), \
|
||||||
"Expand more button should be present"
|
"Expand more button should be present"
|
||||||
mp.click_events_panel_expand_more_button()
|
mp.click_events_panel_expand_more_button()
|
||||||
mp.wait_for_timeout(500)
|
mp.wait_for_timeout(500)
|
||||||
|
|
||||||
events_panel_position = mp.get_events_panel_position()
|
events_panel_position = mp.get_events_panel_position()
|
||||||
assert events_panel_position == "top",\
|
assert events_panel_position == "top",\
|
||||||
"Panel with actions events should be located on the main page top"
|
"Panel with actions events should be located on the main page top"
|
||||||
|
|
||||||
is_scrollable = actions_events_container.check_events_table_verticall_scrolling()
|
is_scrollable = actions_events_container.check_events_table_verticall_scrolling()
|
||||||
assert is_scrollable, "Actions events table should be scrollable in the full window"
|
assert is_scrollable, "Actions events table should be scrollable in the full window"
|
||||||
|
|
||||||
# Скроллинг вниз
|
# Скроллинг вниз
|
||||||
actions_events_container.scroll_events_table_down()
|
actions_events_container.scroll_events_table_down()
|
||||||
browser.wait_for_timeout(1000)
|
browser.wait_for_timeout(1000)
|
||||||
|
|
||||||
# Проверка видимости последней строки после прокрутки
|
# Проверка видимости последней строки после прокрутки
|
||||||
actions_events_container.check_events_table_last_row_visibility()
|
actions_events_container.check_events_table_last_row_visibility()
|
||||||
|
|
||||||
# Скроллинг вверх
|
# Скроллинг вверх
|
||||||
actions_events_container.scroll_events_table_up()
|
actions_events_container.scroll_events_table_up()
|
||||||
browser.wait_for_timeout(1000)
|
browser.wait_for_timeout(1000)
|
||||||
|
|
||||||
# Проверка видимости первой строки после прокрутки
|
# Проверка видимости первой строки после прокрутки
|
||||||
actions_events_container.check_events_table_first_row_visibility()
|
actions_events_container.check_events_table_first_row_visibility()
|
||||||
|
else:
|
||||||
|
print("Not enough data to check vertical scrolling")
|
||||||
|
|
||||||
# @pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
def test_real_time_task_view(self, browser: Page):
|
def test_real_time_task_view(self, browser: Page):
|
||||||
|
|
@ -174,6 +182,15 @@ class TestActionsEventsContainer:
|
||||||
task_view_window.check_stages_table_headers(stages_table_content[0], expected_task_headers)
|
task_view_window.check_stages_table_headers(stages_table_content[0], expected_task_headers)
|
||||||
stages_table_content.pop(0)
|
stages_table_content.pop(0)
|
||||||
|
|
||||||
|
rows_count = len(stages_table_content)
|
||||||
|
if rows_count != 0:
|
||||||
|
# Проверка выделения строк
|
||||||
|
task_view_window.check_stages_table_row_highlighting(0)
|
||||||
|
if rows_count > 1:
|
||||||
|
task_view_window.check_stages_table_row_highlighting(rows_count - 1)
|
||||||
|
if rows_count > 3:
|
||||||
|
task_view_window.check_stages_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
payload = {"filter": {"page": 1},
|
payload = {"filter": {"page": 1},
|
||||||
"count": 40}
|
"count": 40}
|
||||||
|
|
||||||
|
|
@ -246,7 +263,7 @@ class TestActionsEventsContainer:
|
||||||
|
|
||||||
# @pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
def test_real_time_events_table_pagination(self, browser: Page):
|
def test_real_time_events_table_pagination(self, browser: Page):
|
||||||
"""Проверяет возможность пагинации таблицы событий.
|
"""Проверяет возможность пагинации таблицы событий на примере вкладки 'Реальное время'.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
browser: Экземпляр страницы Playwright.
|
browser: Экземпляр страницы Playwright.
|
||||||
|
|
@ -266,6 +283,7 @@ class TestActionsEventsContainer:
|
||||||
|
|
||||||
# Проверка начального состояния
|
# Проверка начального состояния
|
||||||
tested_pages = 1
|
tested_pages = 1
|
||||||
|
pages = 1
|
||||||
|
|
||||||
payload = {"filter": {"page": 1},
|
payload = {"filter": {"page": 1},
|
||||||
"count": 40}
|
"count": 40}
|
||||||
|
|
@ -357,7 +375,7 @@ class TestActionsEventsContainer:
|
||||||
counter += 1
|
counter += 1
|
||||||
browser.wait_for_timeout(2000)
|
browser.wait_for_timeout(2000)
|
||||||
|
|
||||||
if counter == tested_pages:
|
if counter == tested_pages and counter == pages:
|
||||||
actions_events_container.should_be_final_state()
|
actions_events_container.should_be_final_state()
|
||||||
else:
|
else:
|
||||||
actions_events_container.should_be_all_enabled()
|
actions_events_container.should_be_all_enabled()
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,13 @@ class TestAuditEventsContainer:
|
||||||
# Получение количества строк в таблице
|
# Получение количества строк в таблице
|
||||||
rows_count = audit_events_container.get_events_table_rows_count()
|
rows_count = audit_events_container.get_events_table_rows_count()
|
||||||
|
|
||||||
|
if rows_count != 0:
|
||||||
# Проверка выделения строк
|
# Проверка выделения строк
|
||||||
audit_events_container.check_events_table_row_highlighting(0)
|
audit_events_container.check_events_table_row_highlighting(0)
|
||||||
audit_events_container.check_events_table_row_highlighting(rows_count - 1)
|
if rows_count > 1:
|
||||||
audit_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
audit_events_container.check_events_table_row_highlighting(rows_count - 1)
|
||||||
|
if rows_count > 3:
|
||||||
|
audit_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
def test_events_table_scrolling(self, browser: Page):
|
def test_events_table_scrolling(self, browser: Page):
|
||||||
"""Проверяет возможность скроллинга таблицы событий.
|
"""Проверяет возможность скроллинга таблицы событий.
|
||||||
|
|
@ -180,6 +183,7 @@ class TestAuditEventsContainer:
|
||||||
|
|
||||||
# Проверка начального состояния
|
# Проверка начального состояния
|
||||||
tested_pages = 1
|
tested_pages = 1
|
||||||
|
pages = 1
|
||||||
|
|
||||||
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
||||||
payload = {"table": "default",
|
payload = {"table": "default",
|
||||||
|
|
@ -279,7 +283,7 @@ class TestAuditEventsContainer:
|
||||||
counter += 1
|
counter += 1
|
||||||
browser.wait_for_timeout(2000)
|
browser.wait_for_timeout(2000)
|
||||||
|
|
||||||
if counter == tested_pages:
|
if counter == tested_pages and counter == pages:
|
||||||
audit_events_container.should_be_final_state()
|
audit_events_container.should_be_final_state()
|
||||||
else:
|
else:
|
||||||
audit_events_container.should_be_all_enabled()
|
audit_events_container.should_be_all_enabled()
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,13 @@ class TestAuditEventsContainerSecurity:
|
||||||
# Получение количества строк в таблице
|
# Получение количества строк в таблице
|
||||||
rows_count = security_events_container.get_events_table_rows_count()
|
rows_count = security_events_container.get_events_table_rows_count()
|
||||||
|
|
||||||
# Проверка выделения строк
|
if rows_count != 0:
|
||||||
security_events_container.check_events_table_row_highlighting(0)
|
# Проверка выделения строк
|
||||||
security_events_container.check_events_table_row_highlighting(rows_count - 1)
|
security_events_container.check_events_table_row_highlighting(0)
|
||||||
security_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
if rows_count > 1:
|
||||||
|
security_events_container.check_events_table_row_highlighting(rows_count - 1)
|
||||||
|
if rows_count > 3:
|
||||||
|
security_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
# Выход из системы текущего пользователя
|
# Выход из системы текущего пользователя
|
||||||
mp.do_logout()
|
mp.do_logout()
|
||||||
|
|
@ -243,6 +246,7 @@ class TestAuditEventsContainerSecurity:
|
||||||
|
|
||||||
# Проверка начального состояния
|
# Проверка начального состояния
|
||||||
tested_pages = 1
|
tested_pages = 1
|
||||||
|
pages = 1
|
||||||
|
|
||||||
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
||||||
payload = {"table": "default",
|
payload = {"table": "default",
|
||||||
|
|
@ -343,7 +347,7 @@ class TestAuditEventsContainerSecurity:
|
||||||
counter += 1
|
counter += 1
|
||||||
browser.wait_for_timeout(2000)
|
browser.wait_for_timeout(2000)
|
||||||
|
|
||||||
if counter == tested_pages:
|
if counter == tested_pages and counter == pages:
|
||||||
security_events_container.should_be_final_state()
|
security_events_container.should_be_final_state()
|
||||||
else:
|
else:
|
||||||
security_events_container.should_be_all_enabled()
|
security_events_container.should_be_all_enabled()
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class TestEventPanel:
|
||||||
|
|
||||||
# Проверяем соответствие тултипов информации на кнопках
|
# Проверяем соответствие тултипов информации на кнопках
|
||||||
tooltip_event_counters = mp.get_event_counters_by_tooltips()
|
tooltip_event_counters = mp.get_event_counters_by_tooltips()
|
||||||
button_event_counters = mp.get_event_counters_by_tooltips()
|
button_event_counters = mp.get_event_counters_by_buttons()
|
||||||
|
|
||||||
for event, counter in tooltip_event_counters.items():
|
for event, counter in tooltip_event_counters.items():
|
||||||
button_counter = button_event_counters.get(event)
|
button_counter = button_event_counters.get(event)
|
||||||
|
|
@ -49,7 +49,7 @@ class TestEventPanel:
|
||||||
if button_counter != counter:
|
if button_counter != counter:
|
||||||
assert False, f"Expected tooltip value {counter} is not equal button value {button_counter} for event button {event}"
|
assert False, f"Expected tooltip value {counter} is not equal button value {button_counter} for event button {event}"
|
||||||
|
|
||||||
@pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
def test_event_panel_expand_buttons(self, browser: Page) -> None:
|
def test_event_panel_expand_buttons(self, browser: Page) -> None:
|
||||||
"""Проверяет состояние и количество кнопок расширения рабочей области панели событий.
|
"""Проверяет состояние и количество кнопок расширения рабочей области панели событий.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,13 @@ class TestEventsTabContainer:
|
||||||
# Получение количества строк в таблице
|
# Получение количества строк в таблице
|
||||||
rows_count = events_tab_container.get_events_table_rows_count()
|
rows_count = events_tab_container.get_events_table_rows_count()
|
||||||
|
|
||||||
# Проверка выделения строк
|
if rows_count != 0:
|
||||||
events_tab_container.check_events_table_row_highlighting(0)
|
# Проверка выделения строк
|
||||||
events_tab_container.check_events_table_row_highlighting(rows_count - 1)
|
events_tab_container.check_events_table_row_highlighting(0)
|
||||||
events_tab_container.check_events_table_row_highlighting(int(rows_count / 2))
|
if rows_count > 1:
|
||||||
|
events_tab_container.check_events_table_row_highlighting(rows_count - 1)
|
||||||
|
if rows_count > 3:
|
||||||
|
events_tab_container.check_events_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
@pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
||||||
def test_events_table_scrolling(self, browser: Page):
|
def test_events_table_scrolling(self, browser: Page):
|
||||||
|
|
@ -184,6 +187,7 @@ class TestEventsTabContainer:
|
||||||
|
|
||||||
# Проверка начального состояния
|
# Проверка начального состояния
|
||||||
tested_pages = 1
|
tested_pages = 1
|
||||||
|
pages = 1
|
||||||
|
|
||||||
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
||||||
payload = {"table": "syslogs",
|
payload = {"table": "syslogs",
|
||||||
|
|
@ -277,7 +281,7 @@ class TestEventsTabContainer:
|
||||||
counter += 1
|
counter += 1
|
||||||
browser.wait_for_timeout(2000)
|
browser.wait_for_timeout(2000)
|
||||||
|
|
||||||
if counter == tested_pages:
|
if counter == tested_pages and counter == pages:
|
||||||
events_tab_container.should_be_final_state()
|
events_tab_container.should_be_final_state()
|
||||||
else:
|
else:
|
||||||
events_tab_container.should_be_all_enabled()
|
events_tab_container.should_be_all_enabled()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ class TestMaintenanceEventsContainer:
|
||||||
maintenance_events_container = mp.click_events_panel_maintenance_tab()
|
maintenance_events_container = mp.click_events_panel_maintenance_tab()
|
||||||
maintenance_events_container.check_content()
|
maintenance_events_container.check_content()
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
|
||||||
def test_events_table_row_highlighting(self, browser: Page):
|
def test_events_table_row_highlighting(self, browser: Page):
|
||||||
"""Проверяет выделение строк в таблице событий.
|
"""Проверяет выделение строк в таблице событий.
|
||||||
|
|
||||||
|
|
@ -52,11 +51,13 @@ class TestMaintenanceEventsContainer:
|
||||||
|
|
||||||
# Получение количества строк в таблице
|
# Получение количества строк в таблице
|
||||||
rows_count = maintenance_events_container.get_events_table_rows_count()
|
rows_count = maintenance_events_container.get_events_table_rows_count()
|
||||||
|
if rows_count != 0:
|
||||||
# Проверка выделения строк
|
# Проверка выделения строк
|
||||||
maintenance_events_container.check_events_table_row_highlighting(0)
|
maintenance_events_container.check_events_table_row_highlighting(0)
|
||||||
maintenance_events_container.check_events_table_row_highlighting(rows_count - 1)
|
if rows_count > 1:
|
||||||
maintenance_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
maintenance_events_container.check_events_table_row_highlighting(rows_count - 1)
|
||||||
|
if rows_count > 3:
|
||||||
|
maintenance_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
@pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
||||||
def test_events_table_scrolling(self, browser: Page):
|
def test_events_table_scrolling(self, browser: Page):
|
||||||
|
|
@ -74,6 +75,9 @@ class TestMaintenanceEventsContainer:
|
||||||
|
|
||||||
maintenance_events_container = mp.click_events_panel_maintenance_tab()
|
maintenance_events_container = mp.click_events_panel_maintenance_tab()
|
||||||
|
|
||||||
|
# Получение количества строк в таблице
|
||||||
|
# rows_count = maintenance_events_container.get_events_table_rows_count()
|
||||||
|
|
||||||
events_panel_position = mp.get_events_panel_position()
|
events_panel_position = mp.get_events_panel_position()
|
||||||
|
|
||||||
# Проверка, что панель с таблицей открыта
|
# Проверка, что панель с таблицей открыта
|
||||||
|
|
@ -127,7 +131,7 @@ class TestMaintenanceEventsContainer:
|
||||||
# Проверка видимости первой строки после прокрутки
|
# Проверка видимости первой строки после прокрутки
|
||||||
maintenance_events_container.check_events_table_first_row_visibility()
|
maintenance_events_container.check_events_table_first_row_visibility()
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
# @pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
||||||
def test_events_table_column_sorting(self, browser: Page):
|
def test_events_table_column_sorting(self, browser: Page):
|
||||||
"""Проверяет сортировку колонки 'Время' в таблице событий.
|
"""Проверяет сортировку колонки 'Время' в таблице событий.
|
||||||
|
|
||||||
|
|
@ -164,7 +168,7 @@ class TestMaintenanceEventsContainer:
|
||||||
convert2timestamp=True)
|
convert2timestamp=True)
|
||||||
assert is_descending_order, "Column data should be in descending order"
|
assert is_descending_order, "Column data should be in descending order"
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
# @pytest.mark.skip(reason="Отсутствуют данные для вывода в таблицу событий")
|
||||||
def test_events_table_pagination(self, browser: Page):
|
def test_events_table_pagination(self, browser: Page):
|
||||||
"""Проверяет возможность пагинации таблицы событий.
|
"""Проверяет возможность пагинации таблицы событий.
|
||||||
|
|
||||||
|
|
@ -184,6 +188,7 @@ class TestMaintenanceEventsContainer:
|
||||||
|
|
||||||
# Проверка начального состояния
|
# Проверка начального состояния
|
||||||
tested_pages = 1
|
tested_pages = 1
|
||||||
|
pages = 1
|
||||||
|
|
||||||
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
||||||
payload = {"id": [ "/physical"],
|
payload = {"id": [ "/physical"],
|
||||||
|
|
@ -196,7 +201,11 @@ class TestMaintenanceEventsContainer:
|
||||||
response_body = mp.get_response_body(response)
|
response_body = mp.get_response_body(response)
|
||||||
|
|
||||||
if response_body:
|
if response_body:
|
||||||
pages = response_body["data"]["pages"]
|
pages = int(len(response_body)/40)
|
||||||
|
if (len(response_body) % 40) > 0:
|
||||||
|
pages = pages + 1
|
||||||
|
|
||||||
|
# print(f"pages = {pages}")
|
||||||
|
|
||||||
if pages > 5:
|
if pages > 5:
|
||||||
tested_pages = 5
|
tested_pages = 5
|
||||||
|
|
@ -278,7 +287,7 @@ class TestMaintenanceEventsContainer:
|
||||||
counter += 1
|
counter += 1
|
||||||
browser.wait_for_timeout(2000)
|
browser.wait_for_timeout(2000)
|
||||||
|
|
||||||
if counter == tested_pages:
|
if counter == tested_pages and counter == pages:
|
||||||
maintenance_events_container.should_be_final_state()
|
maintenance_events_container.should_be_final_state()
|
||||||
else:
|
else:
|
||||||
maintenance_events_container.should_be_all_enabled()
|
maintenance_events_container.should_be_all_enabled()
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,13 @@ class TestSystemLogEventsContainer:
|
||||||
# Получение количества строк в таблице
|
# Получение количества строк в таблице
|
||||||
rows_count = system_log_events_container.get_events_table_rows_count()
|
rows_count = system_log_events_container.get_events_table_rows_count()
|
||||||
|
|
||||||
# Проверка выделения строк
|
if rows_count != 0:
|
||||||
system_log_events_container.check_events_table_row_highlighting(0)
|
# Проверка выделения строк
|
||||||
system_log_events_container.check_events_table_row_highlighting(rows_count - 1)
|
system_log_events_container.check_events_table_row_highlighting(0)
|
||||||
system_log_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
if rows_count > 1:
|
||||||
|
system_log_events_container.check_events_table_row_highlighting(rows_count - 1)
|
||||||
|
if rows_count > 3:
|
||||||
|
system_log_events_container.check_events_table_row_highlighting(int(rows_count / 2))
|
||||||
|
|
||||||
def test_events_table_scrolling(self, browser: Page):
|
def test_events_table_scrolling(self, browser: Page):
|
||||||
"""Проверяет возможность скроллинга таблицы событий.
|
"""Проверяет возможность скроллинга таблицы событий.
|
||||||
|
|
@ -185,6 +188,7 @@ class TestSystemLogEventsContainer:
|
||||||
|
|
||||||
# Проверка начального состояния
|
# Проверка начального состояния
|
||||||
tested_pages = 1
|
tested_pages = 1
|
||||||
|
pages = 1
|
||||||
|
|
||||||
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
# to do: некорректный запрос в бэк, должно быть "filter": {"page": 0}
|
||||||
payload = {"table": "logs",
|
payload = {"table": "logs",
|
||||||
|
|
@ -278,7 +282,7 @@ class TestSystemLogEventsContainer:
|
||||||
counter += 1
|
counter += 1
|
||||||
browser.wait_for_timeout(2000)
|
browser.wait_for_timeout(2000)
|
||||||
|
|
||||||
if counter == tested_pages:
|
if counter == tested_pages and counter == pages:
|
||||||
system_log_events_container.should_be_final_state()
|
system_log_events_container.should_be_final_state()
|
||||||
else:
|
else:
|
||||||
system_log_events_container.should_be_all_enabled()
|
system_log_events_container.should_be_all_enabled()
|
||||||
|
|
|
||||||
|
|
@ -157,16 +157,14 @@ class TestEmailNotificationsSettingsTab:
|
||||||
send_test_email_window.close_by_toolbar_button()
|
send_test_email_window.close_by_toolbar_button()
|
||||||
|
|
||||||
# @pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
# TO-DO: rewrite tescase after feature release
|
def test_send_test_email_successful(self, browser: Page) -> None:
|
||||||
def test_send_test_email(self, browser: Page) -> None:
|
|
||||||
"""Тест модального окна для посылки тестового E-mail.
|
"""Тест модального окна для посылки тестового E-mail.
|
||||||
|
|
||||||
Проверяет:
|
Проверяет:
|
||||||
Возможность посылки тестового E-mail.
|
Возможность посылки тестового E-mail по существующему адресу.
|
||||||
"""
|
"""
|
||||||
# Адрес куда отправлять e-mail
|
# Адрес куда отправлять e-mail
|
||||||
# Пока фейковый
|
sent_address = "audiomine.platform@gmail.com"
|
||||||
fake_address = "test@grandpas_village.com"
|
|
||||||
|
|
||||||
# Инициализация вкладки
|
# Инициализация вкладки
|
||||||
email_notification_settings_tab = EmailNotificationsSettingsTab(browser)
|
email_notification_settings_tab = EmailNotificationsSettingsTab(browser)
|
||||||
|
|
@ -176,7 +174,7 @@ class TestEmailNotificationsSettingsTab:
|
||||||
|
|
||||||
send_test_email_window = email_notification_settings_tab.click_test_button()
|
send_test_email_window = email_notification_settings_tab.click_test_button()
|
||||||
|
|
||||||
send_test_email_window.input_email(fake_address)
|
send_test_email_window.input_email(sent_address)
|
||||||
|
|
||||||
with browser.expect_response("**/e-nms/email/testEmail") as response_info:
|
with browser.expect_response("**/e-nms/email/testEmail") as response_info:
|
||||||
send_test_email_window.click_test_button()
|
send_test_email_window.click_test_button()
|
||||||
|
|
@ -187,6 +185,39 @@ class TestEmailNotificationsSettingsTab:
|
||||||
|
|
||||||
send_test_email_window.close()
|
send_test_email_window.close()
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_send_test_email_address_validation(self, browser: Page) -> None:
|
||||||
|
"""Тест модального окна для посылки тестового E-mail.
|
||||||
|
|
||||||
|
Проверяет:
|
||||||
|
Валидацию вводимого адреса E-mail.
|
||||||
|
"""
|
||||||
|
# Адрес куда отправлять e-mail - фейковый
|
||||||
|
incorrect_addresses = ["rrrrr", "@mail.ru", "rrrmail.ru", "rr@mail", "rrrr@@mail.ru", "rr@mailru", "rr@my_mail.ru"]
|
||||||
|
|
||||||
|
# Инициализация вкладки
|
||||||
|
email_notification_settings_tab = EmailNotificationsSettingsTab(browser)
|
||||||
|
|
||||||
|
send_test_email_window = email_notification_settings_tab.click_test_button()
|
||||||
|
|
||||||
|
# Пустое поле ввода адреса
|
||||||
|
send_test_email_window.click_test_button()
|
||||||
|
send_test_email_window.should_be_error_alert('\nПоле должно быть заполнено\n')
|
||||||
|
|
||||||
|
for address in incorrect_addresses:
|
||||||
|
send_test_email_window.input_email(address)
|
||||||
|
send_test_email_window.click_test_button()
|
||||||
|
send_test_email_window.should_be_error_alert('\nНекорректный e-mail\n')
|
||||||
|
|
||||||
|
# нет проверки валидности домена
|
||||||
|
# fake_address = "test@grandpasvillage.com"
|
||||||
|
|
||||||
|
# send_test_email_window.input_email(fake_address)
|
||||||
|
# send_test_email_window.click_test_button()
|
||||||
|
# send_test_email_window.should_be_error_alert('\Ошибка входа в систему\n')
|
||||||
|
|
||||||
|
send_test_email_window.close()
|
||||||
|
|
||||||
def _get_default_value(self, setting_name: str, default_settings: dict) -> str| None:
|
def _get_default_value(self, setting_name: str, default_settings: dict) -> str| None:
|
||||||
for setting in default_settings:
|
for setting in default_settings:
|
||||||
if setting["name"] == setting_name:
|
if setting["name"] == setting_name:
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class TestPushNotificationsSettingsTab:
|
||||||
assert msg_value == expected_msg_value, \
|
assert msg_value == expected_msg_value, \
|
||||||
f"Actual message field value {msg_value} is not equal expected message field value {expected_msg_value}"
|
f"Actual message field value {msg_value} is not equal expected message field value {expected_msg_value}"
|
||||||
|
|
||||||
# @pytest.mark.develop
|
@pytest.mark.develop
|
||||||
def test_send_push_notification(self, browser: Page) -> None:
|
def test_send_push_notification(self, browser: Page) -> None:
|
||||||
"""Тест содержимого вкладки настройки Push уведомлений.
|
"""Тест содержимого вкладки настройки Push уведомлений.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,12 +220,11 @@ class TestCurrentSessionsTab:
|
||||||
Проверяет:
|
Проверяет:
|
||||||
1. Создание нового пользователя
|
1. Создание нового пользователя
|
||||||
2. Вход нового пользователя в систему
|
2. Вход нового пользователя в систему
|
||||||
3. Проверка наличия сеанса нового пользователя
|
3. Вход в систему пользователя admin
|
||||||
4. Выход нового пользователя из системы (logout)
|
4. Проверка наличия сеанса нового пользователя
|
||||||
5. Вход в систему пользователя admin
|
5. Удаление сеанса нового пользователя
|
||||||
6. Удаление сеанса нового пользователя
|
6. Проверка отсутствия сеанса нового пользователя
|
||||||
7. Проверка отсутствия сеанса нового пользователя
|
7. Удаление пользователя выполняется автоматически фикстурой cleanup_users
|
||||||
8. Удаление пользователя выполняется автоматически фикстурой cleanup_users
|
|
||||||
"""
|
"""
|
||||||
user_data = {"name": "TestUserForManualDeletion", "role": "Администратор", "password": "qwerty1234567"}
|
user_data = {"name": "TestUserForManualDeletion", "role": "Администратор", "password": "qwerty1234567"}
|
||||||
|
|
||||||
|
|
@ -445,9 +444,6 @@ class TestCurrentSessionsTab:
|
||||||
# Проверка наличия сеанса в таблице
|
# Проверка наличия сеанса в таблице
|
||||||
sessions_tab.should_be_session_in_table(new_user_token)
|
sessions_tab.should_be_session_in_table(new_user_token)
|
||||||
|
|
||||||
# Выход из системы нового пользователя
|
|
||||||
new_mp.do_logout()
|
|
||||||
|
|
||||||
# Авторизация администратором
|
# Авторизация администратором
|
||||||
admin_lp = LoginPage(browser)
|
admin_lp = LoginPage(browser)
|
||||||
admin_lp.do_login()
|
admin_lp.do_login()
|
||||||
|
|
@ -551,6 +547,8 @@ class TestCurrentSessionsTab:
|
||||||
print("Ожидание 15 минут для автоматического удаления сеанса...")
|
print("Ожидание 15 минут для автоматического удаления сеанса...")
|
||||||
browser.wait_for_timeout(901000) # 15 минут 1 секунда в миллисекундах
|
browser.wait_for_timeout(901000) # 15 минут 1 секунда в миллисекундах
|
||||||
|
|
||||||
|
# TO-DO: Должна быть проверка перехода на страницу логина для текущего пользователя, fix 890
|
||||||
|
|
||||||
# Авторизация администратором
|
# Авторизация администратором
|
||||||
admin_lp = LoginPage(browser)
|
admin_lp = LoginPage(browser)
|
||||||
admin_lp.do_login()
|
admin_lp.do_login()
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class TestSessionSettingsTab:
|
||||||
else:
|
else:
|
||||||
print(f"Error request session setings data from API: {response.status_text}")
|
print(f"Error request session setings data from API: {response.status_text}")
|
||||||
|
|
||||||
#@pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
def test_edit_session_settings(self, browser: Page) -> None:
|
def test_edit_session_settings(self, browser: Page) -> None:
|
||||||
"""Тест проверки возможности редактирования выбранных полей формы настройки времени жизни сеансов.
|
"""Тест проверки возможности редактирования выбранных полей формы настройки времени жизни сеансов.
|
||||||
|
|
||||||
|
|
@ -91,12 +91,14 @@ class TestSessionSettingsTab:
|
||||||
|
|
||||||
session_settings_tab.edit_settings(new_settings)
|
session_settings_tab.edit_settings(new_settings)
|
||||||
|
|
||||||
# temporarily
|
updated_settings = session_settings_tab.get_settings_values()
|
||||||
# updated_settings = session_settings_tab.get_settings_values()
|
|
||||||
|
|
||||||
# for key, value in new_settings.items():
|
for key, value in new_settings.items():
|
||||||
# updated_value = updated_settings.get(key)
|
updated_value = updated_settings.get(key)
|
||||||
# assert updated_value == value, f"{key} updated value {updated_value} is not equal expected value {value}"
|
assert updated_value == value, f"{key} updated value {updated_value} is not equal expected value {value}"
|
||||||
|
|
||||||
|
# temporarily
|
||||||
|
session_settings_tab.click_cancel_button()
|
||||||
|
|
||||||
# @pytest.mark.develop
|
# @pytest.mark.develop
|
||||||
def test_edit_session_setting_by_arrow(self, browser: Page) -> None:
|
def test_edit_session_setting_by_arrow(self, browser: Page) -> None:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,526 @@
|
||||||
|
"""Модуль тестов вкладки 'Резервное копирование'.
|
||||||
|
|
||||||
|
Содержит тесты для проверки корректности отображения
|
||||||
|
и функциональности элементов вкладки настройки времени жизни сеансов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import Page
|
||||||
|
from pages.login_page import LoginPage
|
||||||
|
from pages.main_page import MainPage
|
||||||
|
from pages.backup_settings_tab import BackupSettingsTab
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.mark.smoke
|
||||||
|
class TestBackupSettingsTab:
|
||||||
|
"""Набор тестов для вкладки 'Обслуживание и диагностика/Резервное копирование'.
|
||||||
|
|
||||||
|
Проверяет корректность отображения и функциональность элементов вкладки 'Резервное копирование'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
|
def setup(self, browser: Page) -> None:
|
||||||
|
"""Фикстура для подготовки тестового окружения.
|
||||||
|
|
||||||
|
Выполняет:
|
||||||
|
1. Авторизацию в системе
|
||||||
|
2. Переход на вкладку 'Резервное копирование' через панель навигации
|
||||||
|
"""
|
||||||
|
|
||||||
|
browser.add_init_script("window.__PLAYWRIGHT__ = true;")
|
||||||
|
|
||||||
|
# Авторизация в системе
|
||||||
|
login_page = LoginPage(browser)
|
||||||
|
login_page.do_login()
|
||||||
|
|
||||||
|
# Инициализация главной страницы
|
||||||
|
main_page = MainPage(browser)
|
||||||
|
|
||||||
|
# Проверка и взаимодействие с элементами навигации
|
||||||
|
main_page.should_be_navigation_panel()
|
||||||
|
main_page.click_main_navigation_panel_item("Настройки")
|
||||||
|
main_page.click_subpanel_item("Обслуживание и диагностика")
|
||||||
|
main_page.click_subpanel_item("Резервное копирование")
|
||||||
|
|
||||||
|
@pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_content(self, browser: Page) -> None:
|
||||||
|
"""Тест содержимого вкладки 'Резервное копирование'.
|
||||||
|
|
||||||
|
Проверяет:
|
||||||
|
1. Наличие и корректность элементов интерфейса
|
||||||
|
2. Соответствие содержимого полей формы данным из БД
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
# Проверка элементов интерфейса
|
||||||
|
backup_settings_tab.check_content()
|
||||||
|
|
||||||
|
# запрос текущих установок настройки 'Инвентаризация/Параметры планировщика'
|
||||||
|
expected_inventory_settings = {}
|
||||||
|
cur_settings_response = backup_settings_tab.send_get_api_request("e-cmdb/api/backupcmdb")
|
||||||
|
if cur_settings_response.status == 200:
|
||||||
|
response_body = backup_settings_tab.get_response_body(cur_settings_response)
|
||||||
|
|
||||||
|
if response_body:
|
||||||
|
expected_inventory_settings = response_body[0].copy()
|
||||||
|
|
||||||
|
if len(expected_inventory_settings) == 0:
|
||||||
|
# запрос дефолтных значений настройки 'Инвентаризация/Параметры планировщика'
|
||||||
|
default_settings = {}
|
||||||
|
default_settings_response = backup_settings_tab.send_get_api_request("e-cmdb/api/backupcmdb/meta")
|
||||||
|
if default_settings_response.status == 200:
|
||||||
|
response_body = backup_settings_tab.get_response_body(default_settings_response)
|
||||||
|
|
||||||
|
if response_body:
|
||||||
|
default_settings = response_body["fields"].copy()
|
||||||
|
expected_inventory_settings["auto_backup"] = self._get_default_value("auto_backup",
|
||||||
|
default_settings)
|
||||||
|
expected_inventory_settings["backup_limitation"] = self._get_default_value("backup_limitation",
|
||||||
|
default_settings)
|
||||||
|
|
||||||
|
# Проверка соответствия для значений настройки 'Инвентаризация/Параметры планировщика'
|
||||||
|
inventory_scheduler_settings = backup_settings_tab.get_inventory_scheduler_settings_values()
|
||||||
|
inventory_auto_backup = inventory_scheduler_settings.get("auto_backup")
|
||||||
|
inventory_backup_limitation = inventory_scheduler_settings.get("backup_limitation")
|
||||||
|
|
||||||
|
if inventory_auto_backup:
|
||||||
|
expected = expected_inventory_settings["auto_backup"]
|
||||||
|
assert inventory_auto_backup == expected,\
|
||||||
|
f"Actual value {inventory_auto_backup} \
|
||||||
|
is not equal expected {expected} for field 'Время создания резервной копии'"
|
||||||
|
else:
|
||||||
|
assert False, "No value setting for field 'Время создания резервной копии'"
|
||||||
|
|
||||||
|
if inventory_backup_limitation:
|
||||||
|
expected = expected_inventory_settings["backup_limitation"]
|
||||||
|
assert inventory_backup_limitation == expected,\
|
||||||
|
f"Actual value {inventory_backup_limitation} \
|
||||||
|
is not equal expected {expected} for field 'Количество резервных копий'"
|
||||||
|
else:
|
||||||
|
assert False, "No value setting for field 'Количество резервных копий'"
|
||||||
|
|
||||||
|
# запрос текущих установок настройки 'Потоковые данные'
|
||||||
|
expected_sd_settings = {}
|
||||||
|
cur_settings_response = backup_settings_tab.send_get_api_request("e-cmdb/api/backupstreamingdata")
|
||||||
|
if cur_settings_response.status == 200:
|
||||||
|
response_body = backup_settings_tab.get_response_body(cur_settings_response)
|
||||||
|
|
||||||
|
if response_body:
|
||||||
|
expected_sd_settings = response_body[0].copy()
|
||||||
|
|
||||||
|
if len(expected_sd_settings) == 0:
|
||||||
|
# запрос дефолтных значений настройки 'Потоковые данные'
|
||||||
|
default_sd_settings = {}
|
||||||
|
default_settings_response = backup_settings_tab.send_get_api_request("e-cmdb/api/backupstreamingdata/meta")
|
||||||
|
if default_settings_response.status == 200:
|
||||||
|
response_body = backup_settings_tab.get_response_body(default_settings_response)
|
||||||
|
|
||||||
|
if response_body:
|
||||||
|
default_sd_settings = response_body["fields"].copy()
|
||||||
|
expected_sd_settings["auto_backup"] = self._get_default_value("auto_backup", default_sd_settings)
|
||||||
|
expected_sd_settings["data_limitation_default"] = self._get_default_value(
|
||||||
|
"data_limitation_default", default_sd_settings)
|
||||||
|
expected_sd_settings["interval_limitation_default"] = self._get_default_value(
|
||||||
|
"interval_limitation_default", default_sd_settings)
|
||||||
|
|
||||||
|
expected_sd_settings["data_limitation_logs"] = self._get_default_value("data_limitation_logs",
|
||||||
|
default_sd_settings)
|
||||||
|
expected_sd_settings["interval_limitation_logs"] = self._get_default_value(
|
||||||
|
"interval_limitation_logs", default_sd_settings)
|
||||||
|
expected_sd_settings["data_limitation_metrics"] = self._get_default_value("data_limitation_metrics",
|
||||||
|
default_sd_settings)
|
||||||
|
expected_sd_settings["interval_limitation_metrics"] = self._get_default_value(
|
||||||
|
"interval_limitation_metrics",
|
||||||
|
default_sd_settings)
|
||||||
|
expected_sd_settings["data_limitation_syslog"] = self._get_default_value("data_limitation_syslog",
|
||||||
|
default_sd_settings)
|
||||||
|
expected_sd_settings["interval_limitation_syslog"] = self._get_default_value(
|
||||||
|
"interval_limitation_syslog", default_sd_settings)
|
||||||
|
expected_sd_settings["data_limitation_tasks"] = self._get_default_value("data_limitation_tasks",
|
||||||
|
default_sd_settings)
|
||||||
|
expected_sd_settings["interval_limitation_tasks"] = self._get_default_value(
|
||||||
|
"interval_limitation_tasks", default_sd_settings)
|
||||||
|
|
||||||
|
|
||||||
|
# Проверка соответствия для значений настроек 'Потоковые данные и Параметры планировщика'
|
||||||
|
dates = {"day":"ДЕНЬ", "hour":"ЧАС", "month":"МЕСЯЦ", "year":"ГОД"}
|
||||||
|
streaming_data_scheduler_settings = backup_settings_tab.get_streaming_data_scheduler_settings_values()
|
||||||
|
sd_auto_backup = streaming_data_scheduler_settings.get("auto_backup")
|
||||||
|
streaming_data_settings = backup_settings_tab.get_streaming_data_settings_values()
|
||||||
|
|
||||||
|
if sd_auto_backup:
|
||||||
|
expected = expected_sd_settings["auto_backup"]
|
||||||
|
assert sd_auto_backup == expected,\
|
||||||
|
f"Actual value {sd_auto_backup} \
|
||||||
|
is not equal expected {expected} for field 'Время создания резервной копии'"
|
||||||
|
else:
|
||||||
|
assert False, "No value setting for field 'Время создания резервной копии' streaming data"
|
||||||
|
|
||||||
|
settings_list = streaming_data_settings.keys()
|
||||||
|
for setting in settings_list:
|
||||||
|
expected = expected_sd_settings[setting]
|
||||||
|
if dates.get(expected):
|
||||||
|
expected_setting = dates[expected]
|
||||||
|
else:
|
||||||
|
expected_setting = str(expected)
|
||||||
|
actual = streaming_data_settings[setting]
|
||||||
|
assert actual == expected_setting,\
|
||||||
|
f"Actual value {actual} is not equal expected {expected_setting} for field '{setting}'"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_check_backup_copies_amount(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки количества резервных копий."""
|
||||||
|
|
||||||
|
# TO-DO: Тест проверки правильности времени их создания
|
||||||
|
# To-DO: Ограничение на количество копий для потоковых данных?
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
# получение текущих установок настройки 'Инвентаризация/Параметры планировщика'
|
||||||
|
inventory_scheduler_settings = backup_settings_tab.get_inventory_scheduler_settings_values()
|
||||||
|
#inventory_auto_backup = inventory_scheduler_settings["auto_backup"]
|
||||||
|
inventory_backup_limitation = int(inventory_scheduler_settings["backup_limitation"])
|
||||||
|
|
||||||
|
# получение списка резервных копий настройки 'Инвентаризация'
|
||||||
|
dumps = backup_settings_tab.get_inventory_dumps_list()
|
||||||
|
print(dumps)
|
||||||
|
|
||||||
|
assert inventory_backup_limitation >= len(dumps), \
|
||||||
|
f"Required to store {inventory_backup_limitation} but {len(dumps)} stores"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_create_copy(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки создания резервных копий."""
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
# проверка создания резервной копии для блока 'Инвентаризация'
|
||||||
|
current_date = datetime.now(timezone.utc)
|
||||||
|
current_ts = current_date.timestamp()
|
||||||
|
|
||||||
|
backup_settings_tab.create_inventory_copy()
|
||||||
|
backup_settings_tab.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
dumps_cmdb = backup_settings_tab.get_inventory_dumps_list()
|
||||||
|
max_ts, _ = self._get_last_dump(dumps_cmdb, "inventory")
|
||||||
|
assert max_ts - current_ts < 1000, "New inventory backup copy not found"
|
||||||
|
|
||||||
|
# проверка создания резервной копии для блока 'Потоковые данные'
|
||||||
|
current_date = datetime.now(timezone.utc)
|
||||||
|
current_ts = current_date.timestamp()
|
||||||
|
|
||||||
|
backup_settings_tab.create_streaming_data_copy()
|
||||||
|
backup_settings_tab.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
dumps_streaming_data = backup_settings_tab.get_streaming_data_dumps_list()
|
||||||
|
max_ts, _ = self._get_last_dump(dumps_streaming_data, "streaming_data")
|
||||||
|
assert max_ts - current_ts < 1000, "New streaming_data backup copy not found"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_check_backup_buttons(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки поведения кнопок управления резервными копиями."""
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
dumps_cmdb = backup_settings_tab.get_inventory_dumps_list()
|
||||||
|
max_ts, last_dump = self._get_last_dump(dumps_cmdb, "inventory")
|
||||||
|
|
||||||
|
backup_settings_tab.select_inventory_dump(last_dump)
|
||||||
|
|
||||||
|
backup_settings_tab.should_be_inventory_download_button()
|
||||||
|
is_disabled = backup_settings_tab.check_inventory_restore_copy_button_disabling()
|
||||||
|
assert not is_disabled, "Inventory button to restore copy should be enabled"
|
||||||
|
|
||||||
|
backup_settings_tab.clear_inventory_dump_selection()
|
||||||
|
backup_settings_tab.should_be_inventory_upload_button()
|
||||||
|
is_disabled = backup_settings_tab.check_inventory_restore_copy_button_disabling()
|
||||||
|
assert is_disabled, "Inventory button to restore copy should be disabled"
|
||||||
|
|
||||||
|
dumps_streaming_data = backup_settings_tab.get_streaming_data_dumps_list()
|
||||||
|
max_ts, last_dump = self._get_last_dump(dumps_streaming_data, "streaming_data")
|
||||||
|
|
||||||
|
backup_settings_tab.select_streaming_data_dump(last_dump)
|
||||||
|
|
||||||
|
backup_settings_tab.should_be_streaming_data_download_button()
|
||||||
|
is_disabled = backup_settings_tab.check_streaming_data_restore_copy_button_disabling()
|
||||||
|
assert not is_disabled, "Streaming data button to restore copy should be enabled"
|
||||||
|
|
||||||
|
backup_settings_tab.clear_streaming_data_dump_selection()
|
||||||
|
backup_settings_tab.should_be_streaming_data_upload_button()
|
||||||
|
is_disabled = backup_settings_tab.check_streaming_data_restore_copy_button_disabling()
|
||||||
|
assert is_disabled, "Streaming data button to restore copy should be disabled"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_check_inventory_download_copy(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки возможности загрузки резервной копии 'Инвентаризация'."""
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
path_to_download = Path.home() / "Downloads"
|
||||||
|
|
||||||
|
dumps_cmdb = backup_settings_tab.get_inventory_dumps_list()
|
||||||
|
max_ts, last_dump = self._get_last_dump(dumps_cmdb, "inventory")
|
||||||
|
|
||||||
|
backup_settings_tab.download_inventory_copy(last_dump, path_to_download)
|
||||||
|
|
||||||
|
downloaded = str(path_to_download) + "/" + last_dump
|
||||||
|
|
||||||
|
assert os.path.exists(downloaded), f"The file '{downloaded}' not found"
|
||||||
|
assert os.path.getsize(downloaded) > 0, f"The file '{downloaded}' is empty"
|
||||||
|
|
||||||
|
os.remove(downloaded)
|
||||||
|
|
||||||
|
#@pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_check_streaming_data_download_copy(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки возможности загрузки резервной копии 'Потоковые данные'."""
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
path_to_download = Path.home() / 'Documents'
|
||||||
|
|
||||||
|
dumps_cmdb = backup_settings_tab.get_streaming_data_dumps_list()
|
||||||
|
max_ts, last_dump = self._get_last_dump(dumps_cmdb, "streaming_data")
|
||||||
|
|
||||||
|
backup_settings_tab.download_streaming_data_copy(last_dump, path_to_download)
|
||||||
|
|
||||||
|
downloaded = str(path_to_download) + "/" + last_dump
|
||||||
|
|
||||||
|
assert os.path.exists(downloaded), f"The file '{downloaded}' not found"
|
||||||
|
assert os.path.getsize(downloaded) > 0, f"The file '{downloaded}' is empty"
|
||||||
|
|
||||||
|
os.remove(downloaded)
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_set_inventory_scheduler_settings(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки возможности изменения значения настроек 'Инвентаризация/Параметры планировщика'."""
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
# считываем и запоминаем текущие знчения
|
||||||
|
orig_inventory_scheduler_settings = backup_settings_tab.get_inventory_scheduler_settings_values()
|
||||||
|
orig_auto_backup = orig_inventory_scheduler_settings.get("auto_backup")
|
||||||
|
assert orig_auto_backup, "Сouldn't read the value of 'auto backup' from inventory scheduler settings"
|
||||||
|
orig_backup_limitation = orig_inventory_scheduler_settings.get("backup_limitation")
|
||||||
|
assert orig_backup_limitation, "Сouldn't read the value of 'backup limitation' from inventory scheduler settings"
|
||||||
|
|
||||||
|
# устанавливаем новые значения
|
||||||
|
backup_settings_tab.click_edit_button()
|
||||||
|
|
||||||
|
backup_settings_tab.input_inventory_backup_creation_time("0 0 22 * * 7")
|
||||||
|
|
||||||
|
backup_settings_tab.input_inventory_backups_number("6")
|
||||||
|
backup_settings_tab.decrease_inventory_backups_number()
|
||||||
|
backup_settings_tab.increase_inventory_backups_number()
|
||||||
|
|
||||||
|
backup_settings_tab.click_save_button()
|
||||||
|
|
||||||
|
# проверка ожидаемых значений
|
||||||
|
inventory_scheduler_settings = backup_settings_tab.get_inventory_scheduler_settings_values()
|
||||||
|
inventory_auto_backup = inventory_scheduler_settings["auto_backup"]
|
||||||
|
inventory_backup_limitation = inventory_scheduler_settings["backup_limitation"]
|
||||||
|
assert inventory_auto_backup == "0 0 22 * * 7", \
|
||||||
|
f"Actual value '{inventory_auto_backup}' \
|
||||||
|
is not equal expected '0 0 22 * * 7' for field 'Время создания резервной копии'"
|
||||||
|
assert inventory_backup_limitation == "6", \
|
||||||
|
f"Actual value '{inventory_backup_limitation}' \
|
||||||
|
is not equal expected '6' for field 'Количество резервных копий'"
|
||||||
|
|
||||||
|
# восстановление ранее сохраненных значений
|
||||||
|
backup_settings_tab.click_edit_button()
|
||||||
|
backup_settings_tab.input_inventory_backup_creation_time(orig_auto_backup)
|
||||||
|
backup_settings_tab.input_inventory_backups_number(orig_backup_limitation)
|
||||||
|
backup_settings_tab.click_save_button()
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_backup_settings_tab_set_streaming_data_settings(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки возможности изменения значения настроек 'Потоковые данные'."""
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
# считываем и запоминаем текущие знчения
|
||||||
|
orig_streaming_data_settings = backup_settings_tab.get_streaming_data_settings_values()
|
||||||
|
for name in orig_streaming_data_settings:
|
||||||
|
val = orig_streaming_data_settings.get(name)
|
||||||
|
assert val, f"Сouldn't read the value of '{name}' from inventory scheduler settings"
|
||||||
|
orig_streaming_data_scheduler_settings = backup_settings_tab.get_streaming_data_scheduler_settings_values()
|
||||||
|
orig_auto_backup = orig_streaming_data_scheduler_settings.get("auto_backup")
|
||||||
|
assert orig_auto_backup, "Сouldn't read the value of 'auto backup' from streaming data scheduler settings"
|
||||||
|
|
||||||
|
# устанавливаем новые значения
|
||||||
|
backup_settings_tab.click_edit_button()
|
||||||
|
|
||||||
|
backup_settings_tab.input_audit_time_period("4", "месяц")
|
||||||
|
backup_settings_tab.increase_audit_time_period()
|
||||||
|
backup_settings_tab.decrease_audit_time_period()
|
||||||
|
|
||||||
|
backup_settings_tab.input_logs_time_period("4", "месяц")
|
||||||
|
backup_settings_tab.increase_logs_time_period()
|
||||||
|
backup_settings_tab.decrease_logs_time_period()
|
||||||
|
|
||||||
|
backup_settings_tab.input_metrics_time_period("4", "месяц")
|
||||||
|
backup_settings_tab.increase_metrics_time_period()
|
||||||
|
backup_settings_tab.decrease_metrics_time_period()
|
||||||
|
|
||||||
|
backup_settings_tab.input_syslog_time_period("4", "месяц")
|
||||||
|
backup_settings_tab.increase_syslog_time_period()
|
||||||
|
backup_settings_tab.decrease_syslog_time_period()
|
||||||
|
|
||||||
|
backup_settings_tab.input_tasks_time_period("4", "месяц")
|
||||||
|
backup_settings_tab.increase_tasks_time_period()
|
||||||
|
backup_settings_tab.decrease_tasks_time_period()
|
||||||
|
|
||||||
|
backup_settings_tab.input_streaming_data_backup_creation_time("0 0 22 * * 7")
|
||||||
|
|
||||||
|
backup_settings_tab.click_save_button()
|
||||||
|
|
||||||
|
# проверка ожидаемых значений
|
||||||
|
streaming_data_settings = backup_settings_tab.get_streaming_data_settings_values()
|
||||||
|
data_limitation_default = streaming_data_settings["data_limitation_default"]
|
||||||
|
assert data_limitation_default == "4", \
|
||||||
|
f"Actual value '{data_limitation_default}' is not equal expected '4' for category 'Аудит'"
|
||||||
|
interval_limitation_default = streaming_data_settings["interval_limitation_default"]
|
||||||
|
assert interval_limitation_default == "МЕСЯЦ", \
|
||||||
|
f"Actual value '{interval_limitation_default}' is not equal expected 'месяц' for category 'Аудит'"
|
||||||
|
data_limitation_logs = streaming_data_settings["data_limitation_logs"]
|
||||||
|
assert data_limitation_logs == "4", \
|
||||||
|
f"Actual value '{data_limitation_logs}' is not equal expected '4' for category 'Логи'"
|
||||||
|
interval_limitation_logs = streaming_data_settings["interval_limitation_logs"]
|
||||||
|
assert interval_limitation_logs == "МЕСЯЦ", \
|
||||||
|
f"Actual value '{interval_limitation_logs}' is not equal expected 'месяц' for category 'Логи'"
|
||||||
|
data_limitation_metrics = streaming_data_settings["data_limitation_metrics"]
|
||||||
|
assert data_limitation_metrics == "4", \
|
||||||
|
f"Actual value '{data_limitation_metrics}' is not equal expected '4' for category 'Метрики'"
|
||||||
|
interval_limitation_metrics = streaming_data_settings["interval_limitation_metrics"]
|
||||||
|
assert interval_limitation_metrics == "МЕСЯЦ", \
|
||||||
|
f"Actual value '{interval_limitation_metrics}' is not equal expected 'месяц' for category 'Метрики'"
|
||||||
|
data_limitation_syslog = streaming_data_settings["data_limitation_syslog"]
|
||||||
|
assert data_limitation_syslog == "4", \
|
||||||
|
f"Actual value '{data_limitation_syslog}' is not equal expected '4' for category 'Системный лог'"
|
||||||
|
interval_limitation_syslog = streaming_data_settings["interval_limitation_syslog"]
|
||||||
|
assert interval_limitation_syslog == "МЕСЯЦ", \
|
||||||
|
f"Actual value '{interval_limitation_syslog}' is not equal expected 'месяц' for category 'Системный лог'"
|
||||||
|
data_limitation_tasks = streaming_data_settings["data_limitation_tasks"]
|
||||||
|
assert data_limitation_tasks == "4", \
|
||||||
|
f"Actual value '{data_limitation_tasks}' is not equal expected '4' for category 'Действия'"
|
||||||
|
interval_limitation_tasks = streaming_data_settings["interval_limitation_tasks"]
|
||||||
|
assert interval_limitation_tasks == "МЕСЯЦ", \
|
||||||
|
f"Actual value '{interval_limitation_tasks}' is not equal expected 'месяц' for category 'Действия'"
|
||||||
|
|
||||||
|
streaming_data_scheduler_settings = backup_settings_tab.get_streaming_data_scheduler_settings_values()
|
||||||
|
streaming_data_auto_backup = streaming_data_scheduler_settings["auto_backup"]
|
||||||
|
assert streaming_data_auto_backup == "0 0 22 * * 7", \
|
||||||
|
f"Actual value '{streaming_data_auto_backup}' \
|
||||||
|
is not equal expected '0 0 22 * * 7' for field 'Потоковые данные Время создания резервной копии'"
|
||||||
|
|
||||||
|
# восстановление ранее сохраненных значений
|
||||||
|
backup_settings_tab.click_edit_button()
|
||||||
|
backup_settings_tab.wait_for_timeout(1000)
|
||||||
|
backup_settings_tab.input_audit_time_period(orig_streaming_data_settings["data_limitation_default"],
|
||||||
|
orig_streaming_data_settings["interval_limitation_default"])
|
||||||
|
backup_settings_tab.wait_for_timeout(1000)
|
||||||
|
backup_settings_tab.input_logs_time_period(orig_streaming_data_settings["data_limitation_logs"],
|
||||||
|
orig_streaming_data_settings["interval_limitation_logs"])
|
||||||
|
backup_settings_tab.wait_for_timeout(1000)
|
||||||
|
backup_settings_tab.input_metrics_time_period(orig_streaming_data_settings["data_limitation_metrics"],
|
||||||
|
orig_streaming_data_settings["interval_limitation_metrics"])
|
||||||
|
backup_settings_tab.wait_for_timeout(1000)
|
||||||
|
backup_settings_tab.input_syslog_time_period(orig_streaming_data_settings["data_limitation_syslog"],
|
||||||
|
orig_streaming_data_settings["interval_limitation_syslog"])
|
||||||
|
backup_settings_tab.wait_for_timeout(1000)
|
||||||
|
backup_settings_tab.input_tasks_time_period(orig_streaming_data_settings["data_limitation_tasks"],
|
||||||
|
orig_streaming_data_settings["interval_limitation_tasks"])
|
||||||
|
backup_settings_tab.wait_for_timeout(1000)
|
||||||
|
backup_settings_tab.input_streaming_data_backup_creation_time(orig_auto_backup)
|
||||||
|
backup_settings_tab.wait_for_timeout(1000)
|
||||||
|
backup_settings_tab.click_save_button()
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
@pytest.mark.skip(reason="Временно пока работает неправильно")
|
||||||
|
def test_backup_settings_tab_check_auto_copy_creation(self, browser: Page) -> None:
|
||||||
|
"""Тест проверки создания резервных копий в автоматическом режиме на примере блока 'Инвентаризация'."""
|
||||||
|
|
||||||
|
# делать backup один раз в минуту
|
||||||
|
requested_creation_time = "* */1 * * * *"
|
||||||
|
|
||||||
|
# Инициализация страницы сеансов
|
||||||
|
backup_settings_tab = BackupSettingsTab(browser)
|
||||||
|
|
||||||
|
current_setings = backup_settings_tab.get_inventory_scheduler_settings_values()
|
||||||
|
|
||||||
|
current_creation_time = current_setings.get("auto_backup")
|
||||||
|
assert current_creation_time, "No creation time value for 'Inventory' block"
|
||||||
|
|
||||||
|
current_backup_limitation = current_setings.get("backup_limitation")
|
||||||
|
assert current_backup_limitation, "No backup limitation value for 'Inventory' block"
|
||||||
|
time_to_wait = int(current_backup_limitation) + 1
|
||||||
|
|
||||||
|
backup_settings_tab.click_edit_button()
|
||||||
|
|
||||||
|
backup_settings_tab.input_inventory_backup_creation_time(requested_creation_time)
|
||||||
|
backup_settings_tab.click_save_button()
|
||||||
|
|
||||||
|
current_date = datetime.now(timezone.utc)
|
||||||
|
current_ts = current_date.timestamp()
|
||||||
|
|
||||||
|
backup_settings_tab.wait_for_timeout(time_to_wait*60000)
|
||||||
|
|
||||||
|
backup_settings_tab.click_edit_button()
|
||||||
|
|
||||||
|
backup_settings_tab.input_inventory_backup_creation_time(current_creation_time)
|
||||||
|
backup_settings_tab.click_save_button()
|
||||||
|
|
||||||
|
dumps_cmdb = backup_settings_tab.get_inventory_dumps_list()
|
||||||
|
dumps_amount = len(dumps_cmdb)
|
||||||
|
assert dumps_amount == int(current_backup_limitation), \
|
||||||
|
f"Should be {current_backup_limitation} dumps but got {dumps_amount}"
|
||||||
|
|
||||||
|
for dump in dumps_cmdb:
|
||||||
|
dump_date = dump.replace("dump_cmdb_", "").replace(".dump", "")
|
||||||
|
date_object = datetime.strptime(dump_date, "%Y-%m-%d_%H_%M_%S")
|
||||||
|
ts = date_object.timestamp()
|
||||||
|
assert current_ts < ts, f"Old backup copy {dump} in backups list"
|
||||||
|
|
||||||
|
# Вспомогательные функции
|
||||||
|
def _get_default_value(self, setting_name: str, default_settings: dict) -> str| None:
|
||||||
|
""" Выбор дефолтного значения из списка по его имени """
|
||||||
|
|
||||||
|
for setting in default_settings:
|
||||||
|
if setting["name"] == setting_name:
|
||||||
|
return setting["default"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_last_dump(self, dumps_list: list[str],settings_type : str) -> list:
|
||||||
|
""" Выбор последнего по времени дампа из списка.
|
||||||
|
Возвращает timestamp и имя дампа.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dumps = {}
|
||||||
|
ret_values = []
|
||||||
|
for dump in dumps_list:
|
||||||
|
if settings_type == "inventory":
|
||||||
|
dump_date = dump.replace("dump_cmdb_", "").replace(".dump", "")
|
||||||
|
date_object = datetime.strptime(dump_date, "%Y-%m-%d_%H_%M_%S")
|
||||||
|
elif settings_type == "streaming_data":
|
||||||
|
dump_date = dump.replace("monitoring_backup_", "").replace(".zip", "")
|
||||||
|
date_object = datetime.strptime(dump_date, "%Y-%m-%d_%H-%M-%S")
|
||||||
|
else:
|
||||||
|
assert False, "Unsupported backup setting type"
|
||||||
|
ts = date_object.timestamp()
|
||||||
|
dumps[ts] = dump
|
||||||
|
max_ts = sorted(dumps.keys())[-1]
|
||||||
|
ret_values.append(max_ts)
|
||||||
|
ret_values.append(dumps[max_ts])
|
||||||
|
return ret_values
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
"""Модуль тестов вкладки 'Сертификаты'.
|
||||||
|
|
||||||
|
Содержит тесты для проверки корректности отображения
|
||||||
|
и функциональности элементов вкладки 'Сертификаты'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import string
|
||||||
|
import ssl
|
||||||
|
import random
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import jsondiff
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import Page
|
||||||
|
from pages.login_page import LoginPage
|
||||||
|
from pages.main_page import MainPage
|
||||||
|
from pages.certificates_tab import CertificatesTab
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.mark.smoke
|
||||||
|
class TestCertificatesTab:
|
||||||
|
"""Набор тестов для вкладки 'Обслуживание и диагностика/Сертификаты'.
|
||||||
|
|
||||||
|
Проверяет корректность отображения и функциональность элементов вкладки 'Сертификаты'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
|
def setup(self, browser: Page) -> None:
|
||||||
|
"""Фикстура для подготовки тестового окружения.
|
||||||
|
|
||||||
|
Выполняет:
|
||||||
|
1. Авторизацию в системе
|
||||||
|
2. Переход на вкладку 'Сертификаты' через панель навигации
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Авторизация в системе
|
||||||
|
login_page = LoginPage(browser)
|
||||||
|
login_page.do_login()
|
||||||
|
|
||||||
|
# Инициализация главной страницы
|
||||||
|
main_page = MainPage(browser)
|
||||||
|
|
||||||
|
# Проверка и взаимодействие с элементами навигации
|
||||||
|
main_page.should_be_navigation_panel()
|
||||||
|
main_page.click_main_navigation_panel_item("Настройки")
|
||||||
|
main_page.click_subpanel_item("Обслуживание и диагностика")
|
||||||
|
main_page.click_subpanel_item("Сертификаты")
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_certificates_tab_content(self, browser: Page) -> None:
|
||||||
|
"""Тест содержимого вкладки 'Сертификаты'.
|
||||||
|
|
||||||
|
Проверяет:
|
||||||
|
Наличие и корректность элементов интерфейса
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Инициализация страницы
|
||||||
|
certificates_tab = CertificatesTab(browser)
|
||||||
|
|
||||||
|
# Проверка элементов интерфейса
|
||||||
|
certificates_tab.check_content()
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_certificates_tab_check_viewed_certificate(self, browser: Page) -> None:
|
||||||
|
"""Проверка соответствия выводимого сертификата информации из базы данных."""
|
||||||
|
|
||||||
|
# Инициализация страницы
|
||||||
|
certificates_tab = CertificatesTab(browser)
|
||||||
|
|
||||||
|
certificates_tab.click_certificate_tab_button()
|
||||||
|
certificates_tab.wait_for_timeout(5000)
|
||||||
|
viewed_certificate = certificates_tab.get_certificate()
|
||||||
|
|
||||||
|
db_certificate_response = certificates_tab.send_get_api_request("api/certs/infoCert")
|
||||||
|
if db_certificate_response.status == 200:
|
||||||
|
response_body = certificates_tab.get_response_body(db_certificate_response)
|
||||||
|
|
||||||
|
if response_body:
|
||||||
|
response_body_json = json.dumps(response_body, ensure_ascii=False)
|
||||||
|
viewed_certificate_json = json.dumps(viewed_certificate, ensure_ascii=False)
|
||||||
|
|
||||||
|
diff = jsondiff.diff(response_body_json, viewed_certificate_json, syntax='symmetric')
|
||||||
|
assert len(diff) == 0, "Viewed certificate does not match the one taken from DB. DIFF is {diff}"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_certificates_tab_check_exported_certificate(self, browser: Page) -> None:
|
||||||
|
"""Проверка соответствия выводимого сертификата и содержимого экспортированного сертификата."""
|
||||||
|
|
||||||
|
# Инициализация страницы
|
||||||
|
certificates_tab = CertificatesTab(browser)
|
||||||
|
|
||||||
|
certificates_tab.click_certificate_tab_button()
|
||||||
|
|
||||||
|
viewed_certificate = certificates_tab.get_certificate()
|
||||||
|
|
||||||
|
cert_file = certificates_tab.export_certificate()
|
||||||
|
|
||||||
|
try:
|
||||||
|
exported_certificate = ssl._ssl._test_decode_cert(cert_file)
|
||||||
|
except Exception as e:
|
||||||
|
assert False, f"Error decoding certificate {cert_file}: {e}"
|
||||||
|
else:
|
||||||
|
self._compare_certificates(exported_certificate, viewed_certificate)
|
||||||
|
finally:
|
||||||
|
# Удаление экспортированного файла
|
||||||
|
if os.path.exists(cert_file):
|
||||||
|
os.remove(cert_file)
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_certificates_tab_check_import_certificate_input(self, browser: Page) -> None:
|
||||||
|
"""Частичная проверка действий при импорте сертификата."""
|
||||||
|
|
||||||
|
password = "12345"
|
||||||
|
|
||||||
|
# Инициализация страницы
|
||||||
|
certificates_tab = CertificatesTab(browser)
|
||||||
|
|
||||||
|
certificates_tab.click_import_tab_button()
|
||||||
|
|
||||||
|
assert certificates_tab.is_import_button_disabled(), "Import certificate button should be disabled"
|
||||||
|
|
||||||
|
certificates_tab.input_password_field(password)
|
||||||
|
assert not certificates_tab.is_import_button_disabled(), "Import certificate button should be enabled"
|
||||||
|
|
||||||
|
actual_password = certificates_tab.get_password_field_value()
|
||||||
|
assert actual_password == password, \
|
||||||
|
f"Actual password input field value {actual_password} is not equal expected value {password}"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_certificates_tab_check_reissue_certificate_input(self, browser: Page) -> None:
|
||||||
|
"""Проверка заполнения полей при перевыпуске сертификата."""
|
||||||
|
|
||||||
|
input_values = {"CN":"Entcor-e", "O":"Entcor-e", "OU":"Entcor-e",
|
||||||
|
"C":"RU", "ST":"Moscow", "L":"Moscow"}
|
||||||
|
|
||||||
|
# Инициализация страницы
|
||||||
|
certificates_tab = CertificatesTab(browser)
|
||||||
|
|
||||||
|
certificates_tab.click_reissue_tab_button()
|
||||||
|
|
||||||
|
assert certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
|
||||||
|
certificates_tab.input_identification_cert_name_field(input_values["CN"])
|
||||||
|
certificates_tab.input_identification_organization_field(input_values["O"])
|
||||||
|
certificates_tab.input_identification_org_unit_field(input_values["OU"])
|
||||||
|
certificates_tab.input_location_country_field(input_values["C"])
|
||||||
|
certificates_tab.input_location_state_field(input_values["ST"])
|
||||||
|
certificates_tab.input_location_city_field(input_values["L"])
|
||||||
|
|
||||||
|
assert not certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be enabled"
|
||||||
|
|
||||||
|
actual_identification_fields_values = certificates_tab.get_identification_fields_values()
|
||||||
|
actual_location_fields_values = certificates_tab.get_location_fields_values()
|
||||||
|
|
||||||
|
val = actual_identification_fields_values.get("CN")
|
||||||
|
assert val == input_values["CN"], \
|
||||||
|
f"Actual value for field 'CN' {val} is not equal expected {input_values['CN']}"
|
||||||
|
val = actual_identification_fields_values.get("O")
|
||||||
|
assert val == input_values["O"], \
|
||||||
|
f"Actual value for field 'O' {val} is not equal expected {input_values['O']}"
|
||||||
|
val = actual_identification_fields_values.get("OU")
|
||||||
|
assert val == input_values["OU"], \
|
||||||
|
f"Actual value for field 'OU' {val} is not equal expected {input_values['OU']}"
|
||||||
|
val = actual_location_fields_values.get("C")
|
||||||
|
assert val == input_values["C"], \
|
||||||
|
f"Actual value for field 'C' {val} is not equal expected {input_values['C']}"
|
||||||
|
val = actual_location_fields_values.get("ST")
|
||||||
|
assert val == input_values["ST"], \
|
||||||
|
f"Actual value for field 'ST' {val} is not equal expected {input_values['ST']}"
|
||||||
|
val = actual_location_fields_values.get("L")
|
||||||
|
assert val == input_values["L"], \
|
||||||
|
f"Actual value for field 'L' {val} is not equal expected {input_values['L']}"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_certificates_tab_check_reissue_certificate_input_incorrect(self, browser: Page) -> None:
|
||||||
|
"""Проверка некорректного заполнения полей при перевыпуске сертификата."""
|
||||||
|
|
||||||
|
# Инициализация страницы
|
||||||
|
certificates_tab = CertificatesTab(browser)
|
||||||
|
|
||||||
|
certificates_tab.click_reissue_tab_button()
|
||||||
|
|
||||||
|
assert certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
|
||||||
|
cert_name = self._generate_random_string(65)
|
||||||
|
certificates_tab.input_identification_cert_name_field(cert_name)
|
||||||
|
|
||||||
|
identification_fields_values = certificates_tab.get_identification_fields_values()
|
||||||
|
val = identification_fields_values.get("CN")
|
||||||
|
|
||||||
|
assert len(val) == 64, "Field 'Certificate Name' should be no more than 64 characters long"
|
||||||
|
|
||||||
|
certificates_tab.input_location_country_field("R")
|
||||||
|
certificates_tab.input_location_state_field("")
|
||||||
|
certificates_tab.check_alert('error',
|
||||||
|
'Поле СТРАНА (С) должно \n содержать 2 символа')
|
||||||
|
|
||||||
|
certificates_tab.input_location_country_field("RUS")
|
||||||
|
location_fields_values = certificates_tab.get_location_fields_values()
|
||||||
|
val = location_fields_values.get("C")
|
||||||
|
|
||||||
|
assert val == "RU", "Field 'Country' should be only 2 characters long"
|
||||||
|
|
||||||
|
assert certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_certificates_tab_check_reissue_certificate_input_mandatory_fields(self, browser: Page) -> None:
|
||||||
|
"""Проверка некорректного заполнения полей при перевыпуске сертификата."""
|
||||||
|
|
||||||
|
# Инициализация страницы
|
||||||
|
certificates_tab = CertificatesTab(browser)
|
||||||
|
|
||||||
|
certificates_tab.click_reissue_tab_button()
|
||||||
|
|
||||||
|
assert certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
|
||||||
|
# Кнопка перевыпуска сертификата становится активной только после заполнения обязательных полей
|
||||||
|
cert_name = self._generate_random_string(15)
|
||||||
|
certificates_tab.input_identification_cert_name_field(cert_name)
|
||||||
|
assert certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
|
||||||
|
org_name = self._generate_random_string(5)
|
||||||
|
certificates_tab.input_identification_organization_field(org_name)
|
||||||
|
assert certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
|
||||||
|
certificates_tab.input_location_country_field("RU")
|
||||||
|
assert not certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be enabled"
|
||||||
|
|
||||||
|
certificates_tab.input_identification_organization_field("")
|
||||||
|
assert certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be disabled"
|
||||||
|
|
||||||
|
org_name = self._generate_random_string(5)
|
||||||
|
certificates_tab.input_identification_organization_field(org_name)
|
||||||
|
assert not certificates_tab.is_reissue_button_disabled(), "Reissue certificate button should be enabled"
|
||||||
|
|
||||||
|
# Вспомогательные функции
|
||||||
|
def _compare_certificates(self, exported: dict, viewed: dict) -> None:
|
||||||
|
""" Сравнение содержимого отображаемого сертификата и экпортированного """
|
||||||
|
|
||||||
|
fields = {"countryName":"C", "stateOrProvinceName":"ST", "localityName":"L",
|
||||||
|
"organizationName":"O", "commonName":"CN"}
|
||||||
|
|
||||||
|
version = "v" + str(exported["version"])
|
||||||
|
viewed_version = viewed["baseInfo"]["version"]
|
||||||
|
assert viewed_version == version, \
|
||||||
|
f"Viewed certificate version {viewed_version} is not equal exported certificate version {version}"
|
||||||
|
|
||||||
|
serial_number = exported["serialNumber"]
|
||||||
|
viewed_serial_number = viewed["baseInfo"]["serialNumber"].upper()
|
||||||
|
assert viewed_serial_number == serial_number, \
|
||||||
|
f"Viewed certificate serial number {viewed_serial_number} is not equal exported \
|
||||||
|
certificate serial number {serial_number}"
|
||||||
|
|
||||||
|
not_before = exported["notBefore"]
|
||||||
|
time_string = viewed["validity"]["notBefore"]
|
||||||
|
datetime_object = datetime.fromisoformat(time_string.replace('Z', '+00:00' if not time_string.endswith('+00:00') else ''))
|
||||||
|
viewed_not_before = datetime_object.strftime("%B %d %H:%M:%S %Y") + " GMT"
|
||||||
|
assert viewed_version == version, \
|
||||||
|
f"Viewed certificate validity not before {viewed_not_before} is not equal exported certificate {not_before}"
|
||||||
|
|
||||||
|
not_after = exported["notAfter"]
|
||||||
|
time_string = viewed["validity"]["notAfter"]
|
||||||
|
datetime_object = datetime.fromisoformat(time_string.replace('Z', '+00:00' if not time_string.endswith('+00:00') else ''))
|
||||||
|
viewed_not_after = datetime_object.strftime("%B %d %H:%M:%S %Y") + " GMT"
|
||||||
|
assert viewed_version == version, \
|
||||||
|
f"Viewed certificate validity not after {viewed_not_after} is not equal exported certificate {not_after}"
|
||||||
|
|
||||||
|
count = len(exported["subject"])
|
||||||
|
for i in range(count):
|
||||||
|
name = exported["subject"][i][0][0]
|
||||||
|
field = fields.get(name)
|
||||||
|
if field:
|
||||||
|
val = exported["subject"][i][0][1]
|
||||||
|
viewed_val = viewed["subject"][field]
|
||||||
|
assert viewed_val == val, \
|
||||||
|
f"Viewed certificate field {field} value {viewed_val} is not equal exported certificate {val}"
|
||||||
|
|
||||||
|
def _generate_random_string(self, length):
|
||||||
|
# Набор символов: ascii_letters + digits (буквы и цифры)
|
||||||
|
characters = string.ascii_letters + string.digits
|
||||||
|
# Выбираем случайные символы length раз
|
||||||
|
random_string = ''.join(random.choices(characters, k=length))
|
||||||
|
return random_string
|
||||||
|
|
@ -106,7 +106,7 @@ class TestNavigationPanel:
|
||||||
mp.wait_for_timeout(3000)
|
mp.wait_for_timeout(3000)
|
||||||
|
|
||||||
# Переходим к Стойке
|
# Переходим к Стойке
|
||||||
mp.click_subpanel_item("Test-Rack-01")
|
mp.click_subpanel_item("Test-Rack-02")
|
||||||
mp.wait_for_timeout(5000)
|
mp.wait_for_timeout(5000)
|
||||||
|
|
||||||
# Переходим Здание ЦОД 4
|
# Переходим Здание ЦОД 4
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,8 @@ class TestUsersTabEditUser:
|
||||||
ut = UsersTab(browser)
|
ut = UsersTab(browser)
|
||||||
|
|
||||||
# Удаляем тестовых пользователей
|
# Удаляем тестовых пользователей
|
||||||
test_users = ["TestUser", "TestUserAutoOperator", "TestUserAutoAdmin"]
|
test_users = ["TestUser", "TestUserAutoOperator", "TestUserAutoAdmin",
|
||||||
|
"TestUserTestOperator", "TestUserToBlock"]
|
||||||
|
|
||||||
for user_name in test_users:
|
for user_name in test_users:
|
||||||
# Проверяем существует ли пользователь и удаляем его
|
# Проверяем существует ли пользователь и удаляем его
|
||||||
|
|
@ -167,6 +168,8 @@ class TestUsersTabEditUser:
|
||||||
|
|
||||||
if len(new_password) == 0:
|
if len(new_password) == 0:
|
||||||
assert False, "Unsuccessful password reset"
|
assert False, "Unsuccessful password reset"
|
||||||
|
ut.close_edit_user_window(user_data["name"])
|
||||||
|
mp.do_logout()
|
||||||
|
|
||||||
new_lp = LoginPage(browser)
|
new_lp = LoginPage(browser)
|
||||||
new_lp.do_login(username=user_data["name"], password=new_password)
|
new_lp.do_login(username=user_data["name"], password=new_password)
|
||||||
|
|
@ -214,3 +217,85 @@ class TestUsersTabEditUser:
|
||||||
mp.click_subpanel_item("Пользователи")
|
mp.click_subpanel_item("Пользователи")
|
||||||
mp.click_subpanel_item("Пользователи")
|
mp.click_subpanel_item("Пользователи")
|
||||||
ut.should_not_be_user_in_table(user_data["name"], new_user_data["role"])
|
ut.should_not_be_user_in_table(user_data["name"], new_user_data["role"])
|
||||||
|
|
||||||
|
def test_edit_user_name(self, browser: Page, cleanup_users: None) -> None:
|
||||||
|
"""Проверяет изменение имени пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
browser: Экземпляр страницы Playwright.
|
||||||
|
cleanup_users: Фикстура для автоматического удаления пользователя после теста.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_data: Dict[str, str] = {"name": "TestUserAutoOperator", "role": "Оператор", "password": "1232456789abcd"}
|
||||||
|
|
||||||
|
mp = MainPage(browser)
|
||||||
|
ut = UsersTab(browser)
|
||||||
|
browser.wait_for_timeout(500)
|
||||||
|
|
||||||
|
ut.open_add_user_window()
|
||||||
|
ut.add_new_user(user_data)
|
||||||
|
mp.click_subpanel_item("Пользователи")
|
||||||
|
mp.click_subpanel_item("Пользователи")
|
||||||
|
ut.open_edit_user_page_by_user(user_data["name"], user_data["role"])
|
||||||
|
new_user_data = {"name": "TestUserTestOperator"}
|
||||||
|
ut.edit_user(user_data["name"], new_user_data)
|
||||||
|
mp.click_subpanel_item("Пользователи")
|
||||||
|
mp.click_subpanel_item("Пользователи")
|
||||||
|
ut.should_be_user_in_table(new_user_data["name"], user_data["role"])
|
||||||
|
ut.open_edit_user_page_by_user(new_user_data["name"], user_data["role"])
|
||||||
|
ut.delete_user(new_user_data["name"])
|
||||||
|
mp.click_subpanel_item("Пользователи")
|
||||||
|
mp.click_subpanel_item("Пользователи")
|
||||||
|
ut.should_not_be_user_in_table(new_user_data["name"], user_data["role"])
|
||||||
|
|
||||||
|
# @pytest.mark.develop
|
||||||
|
def test_block_user(self, browser: Page, cleanup_users: None) -> None:
|
||||||
|
"""Проверяет возможность блокировки пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
browser: Экземпляр страницы Playwright.
|
||||||
|
cleanup_users: Фикстура для автоматического удаления пользователя после теста.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_data: Dict[str, str] = {"name": "TestUserToBlock", "role": "Администратор", "password": "123456789abcd"}
|
||||||
|
|
||||||
|
mp = MainPage(browser)
|
||||||
|
ut = UsersTab(browser)
|
||||||
|
browser.wait_for_timeout(500)
|
||||||
|
|
||||||
|
ut.open_add_user_window()
|
||||||
|
ut.add_new_user(user_data)
|
||||||
|
mp.do_logout()
|
||||||
|
|
||||||
|
lp_to_check = LoginPage(browser)
|
||||||
|
lp_to_check.do_login(username=user_data["name"], password=user_data["password"])
|
||||||
|
mp_to_check = MainPage(browser)
|
||||||
|
mp_to_check.do_logout()
|
||||||
|
|
||||||
|
admin_lp = LoginPage(browser)
|
||||||
|
admin_lp.do_login()
|
||||||
|
admin_mp = MainPage(browser)
|
||||||
|
admin_mp.should_be_navigation_panel()
|
||||||
|
admin_mp.click_main_navigation_panel_item("Настройки")
|
||||||
|
admin_mp.click_subpanel_item("Пользователи")
|
||||||
|
admin_ut = UsersTab(browser)
|
||||||
|
admin_ut.open_edit_user_page_by_user(user_data["name"], user_data["role"])
|
||||||
|
new_user_data = {"blocking_checked": True}
|
||||||
|
admin_ut.edit_user(user_data["name"], new_user_data)
|
||||||
|
admin_mp.do_logout()
|
||||||
|
|
||||||
|
lp_to_check_blocked = LoginPage(browser)
|
||||||
|
lp_to_check_blocked.do_unsuccessful_login(username=user_data["name"], password=user_data["password"])
|
||||||
|
|
||||||
|
admin_lp_1 = LoginPage(browser)
|
||||||
|
admin_lp_1.do_login()
|
||||||
|
admin_mp_1 = MainPage(browser)
|
||||||
|
admin_mp_1.should_be_navigation_panel()
|
||||||
|
admin_mp_1.click_main_navigation_panel_item("Настройки")
|
||||||
|
admin_mp_1.click_subpanel_item("Пользователи")
|
||||||
|
admin_ut_1 = UsersTab(browser)
|
||||||
|
admin_ut_1.open_edit_user_page_by_user(user_data["name"], user_data["role"])
|
||||||
|
admin_ut_1.delete_user(user_data["name"])
|
||||||
|
admin_mp_1.click_subpanel_item("Пользователи")
|
||||||
|
admin_mp_1.click_subpanel_item("Пользователи")
|
||||||
|
admin_ut_1.should_not_be_user_in_table(user_data["name"], user_data["role"])
|
||||||
|
|
|
||||||
|
|
@ -59,3 +59,23 @@ class TestUsersTab:
|
||||||
|
|
||||||
ut = UsersTab(browser)
|
ut = UsersTab(browser)
|
||||||
ut.should_be_toolbar_buttons()
|
ut.should_be_toolbar_buttons()
|
||||||
|
|
||||||
|
def test_users_tab_check_row_highlighting(self, browser: Page) -> None:
|
||||||
|
"""Проверяет выделение цветом указанной строки таблицы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
browser: Экземпляр страницы Playwright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ut = UsersTab(browser)
|
||||||
|
|
||||||
|
# Проверка наличия таблицы пользователей
|
||||||
|
ut.should_be_users_table()
|
||||||
|
|
||||||
|
# Получение количества строк в таблице
|
||||||
|
rows_count = ut.get_rows_count()
|
||||||
|
|
||||||
|
# Проверка выделения строк
|
||||||
|
ut.check_users_table_row_highlighting(0)
|
||||||
|
ut.check_users_table_row_highlighting(int(rows_count / 2))
|
||||||
|
ut.check_users_table_row_highlighting(rows_count - 1)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue