From da8bde15a1db2b9319a271846b1e3df34873256c Mon Sep 17 00:00:00 2001 From: Radislav Date: Fri, 6 Mar 2026 11:16:19 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=80=D0=B5=D0=BE=D1=80=D0=B3?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D1=8B=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена новая архитектура: makers/forms/frames - Созданы папки: - makers/ - сборщики интерфейса - forms/ - формы с полями - frames/ - обертки и контейнеры - Удалены устаревшие файлы из components_derived: - rack_maker.py (перенесен в makers/accounting_objects/) - create_child_element_frame.py (перенесен в frames/) - modal_edit_rack.py (заменен на rack_edit_maker.py) - Удалены неиспользуемые страницы создания элементов - Добавлен новый тест test_create_rack.py - Пбновлены существующие тесты test_edit_rack.py и test_management_rack.py - Исправлены локаторы в rack_locators.py - Обновлен alert_component.py Все тесты стойки проходят успешно Связано: переход на архитектуру Maker/Form/Frame --- components/alert_component.py | 9 +- .../accounting_objects/rack_maker.py | 331 --------- .../create_rack_form.cpython-313.pyc | Bin 0 -> 24222 bytes .../edit_rack_form.cpython-313.pyc | Bin 0 -> 29736 bytes forms/create_rack_form.py | 520 ++++++++++++++ forms/edit_rack_form.py | 649 +++++++++++++++++ .../create_element_frame.cpython-313.pyc | Bin 0 -> 16412 bytes .../create_element_frame.py | 81 +-- locators/rack_locators.py | 67 +- .../edit_rack_maker.cpython-313.pyc | Bin 0 -> 49906 bytes .../edit_rack_maker.py | 368 ++-------- .../create_child_element_tab.py | 355 --------- .../create_rack_element_tab.py | 678 ------------------ pages/rack_tab/rack_tab.py | 466 ------------ .../test_create_rack_element.py | 609 ---------------- tests/e2e/elements/test_create_rack.py | 474 ++++++++++++ tests/e2e/elements/test_edit_rack.py | 394 +++++----- tests/e2e/elements/test_management_rack.py | 324 +++++---- 18 files changed, 2159 insertions(+), 3166 deletions(-) delete mode 100644 components_derived/accounting_objects/rack_maker.py create mode 100644 forms/__pycache__/create_rack_form.cpython-313.pyc create mode 100644 forms/__pycache__/edit_rack_form.cpython-313.pyc create mode 100644 forms/create_rack_form.py create mode 100644 forms/edit_rack_form.py create mode 100644 frames/__pycache__/create_element_frame.cpython-313.pyc rename components_derived/frames/create_child_element_frame.py => frames/create_element_frame.py (79%) create mode 100644 makers/__pycache__/edit_rack_maker.cpython-313.pyc rename components_derived/modal_edit_rack.py => makers/edit_rack_maker.py (76%) delete mode 100644 pages/create_elements_tab/create_child_element_tab.py delete mode 100644 pages/create_elements_tab/create_rack_element_tab.py delete mode 100644 pages/rack_tab/rack_tab.py delete mode 100644 tests/e2e/create_elements/test_create_rack_element.py create mode 100644 tests/e2e/elements/test_create_rack.py diff --git a/components/alert_component.py b/components/alert_component.py index 98547ba..0bffdee 100644 --- a/components/alert_component.py +++ b/components/alert_component.py @@ -123,12 +123,13 @@ class AlertComponent(BaseComponent): ).filter(has_text=text)).to_be_hidden(timeout=timeout), msg logger.info(f"Alert window with text '{text}' successfully disappeared") - def check_alert_presence(self, text: str) -> None: + def check_alert_presence(self, text: str, timeout: int = 30000) -> None: """Проверяет наличие alert-окна с заданным текстом. Args: text: Текст для проверки. Если пустая строка - проверяет только - наличие окна. + наличие окна. + timeout: Время ожидания появления alert в миллисекундах Raises: AssertionError: Если alert-окно не найдено. @@ -136,12 +137,12 @@ class AlertComponent(BaseComponent): msg = "Alert window is missing" if text == "": - expect(self.page.get_by_role(AlertLocators.ALERT_ROLE)).to_be_visible(), msg + expect(self.page.get_by_role(AlertLocators.ALERT_ROLE)).to_be_visible(timeout=timeout), msg logger.info(f"Alert window successfully displayed") else: expect(self.page.get_by_role( AlertLocators.ALERT_ROLE - ).filter(has_text=text)).to_be_visible(), msg + ).filter(has_text=text)).to_be_visible(timeout=timeout), msg logger.info(f"Alert window with text '{text}' successfully displayed") def check_text(self, alert_text: str) -> None: diff --git a/components_derived/accounting_objects/rack_maker.py b/components_derived/accounting_objects/rack_maker.py deleted file mode 100644 index 17cd8c9..0000000 --- a/components_derived/accounting_objects/rack_maker.py +++ /dev/null @@ -1,331 +0,0 @@ -"""Модуль создания объекта 'Стойка'.""" - -from dataclasses import dataclass -from playwright.sync_api import Page, Locator -from tools.logger import get_logger -from locators.rack_locators import RackLocators -from components.base_component import BaseComponent - -logger = get_logger("RACK_MAKER") - -logger.setLevel("INFO") - -@dataclass -class RackData: - """Класс для хранения данных стойки.""" - - name: str - height: str = "42" - depth: str = "1000" - serial: str = "" - inventory: str = "" - comment: str = "" - cable_entry: str = "" - state: str = "" - owner: str = "" - service_org: str = "" - project: str = "" - - -class RackObjectMaker(BaseComponent): - """Компонент для создания и настройки стойки.""" - - def __init__(self, page: Page) -> None: - """ - Инициализирует компонент создания стойки. - - Args: - page (Page): Экземпляр страницы Playwright - """ - - super().__init__(page) - - # Действия: - - def _fill_combobox_field(self, field_name: str, value: str, fields_locators: dict) -> None: - """ - Заполняет combobox поле. - - Args: - field_name (str): Название поля - value (str): Значение для установки - fields_locators (dict): Словарь с найденными полями формы - - Raises: - ValueError: Если поле не найдено в форме - """ - - # Получаем контейнер поля по его названию - field_container = fields_locators.get(field_name) - - if not field_container: - logger.error(f"Field '{field_name}' not found in form. Available fields: {list(fields_locators.keys())}") - raise ValueError(f"Field '{field_name}' not found in form") - - logger.debug(f"Filling field '{field_name}' with value '{value}'...") - - # Прокручиваем до поля - field_container.scroll_into_view_if_needed() - self.wait_for_timeout(300) - - # Проверяем видимость поля - self.check_visibility(field_container, f"Field '{field_name}' not found") - - # Находим кнопку открытия выпадающего списка внутри контейнера поля - open_button = field_container.locator(".v-input__append-inner").first - - # Кликаем для открытия выпадающего списка - open_button.click(force=True) - self.wait_for_timeout(300) - - # Вводим значение из выпадающего списка - dropdown_item_locator = RackLocators.DROPDOWN_ITEM_BY_TEXT.format(value) - element = self.page.locator(dropdown_item_locator).first - - # Скроллим к элементу если нужно - self._scroll_until_element( - self.page.locator(RackLocators.DROPDOWN_LIST).first, - value - ) - self.wait_for_timeout(300) - element.click() - - logger.debug(f"Field '{field_name}' filled successfully") - - def _fill_combobox_fields(self, rack_data: RackData) -> None: - """Заполняет combobox поля.""" - - # Получаем все поля формы - fields_locators = self._get_form_fields() - - # Обязательные поля. - if rack_data.height: - self._fill_combobox_field("Высота в юнитах", rack_data.height, fields_locators) - logger.debug(f"Selected height: {rack_data.height} units") - - if rack_data.depth: - self._fill_combobox_field("Глубина (мм)", rack_data.depth, fields_locators) - logger.debug(f"Selected depth: {rack_data.depth} mm") - - # Опциональные поля. - if rack_data.cable_entry: - self._fill_combobox_field("Ввод кабеля", rack_data.cable_entry, fields_locators) - logger.debug(f"Selected cable entry: {rack_data.cable_entry}") - - if rack_data.state: - self._fill_combobox_field("Состояние", rack_data.state, fields_locators) - logger.debug(f"Selected state: {rack_data.state}") - - if rack_data.owner: - self._fill_combobox_field("Владелец", rack_data.owner, fields_locators) - logger.debug(f"Selected owner: {rack_data.owner}") - - if rack_data.service_org: - self._fill_combobox_field("Обслуживающая организация", rack_data.service_org, fields_locators) - logger.debug(f"Selected service organization: {rack_data.service_org}") - - if rack_data.project: - self._fill_combobox_field("Проект/Титул", rack_data.project, fields_locators) - logger.debug(f"Selected project/title: {rack_data.project}") - - def _fill_text_fields(self, rack_data: RackData) -> None: - """Заполняет текстовые поля.""" - - logger.debug("Filling text fields...") - - # Получаем все поля формы - fields_locators = self._get_form_fields() - - logger.debug(f"Available text fields: {list(fields_locators.keys())}") - - def clear_and_fill(field_name: str, value: str): - """Очищает поле и заполняет его значением.""" - - if not value: - logger.debug(f"Skipping empty value for field '{field_name}'") - return - - # Получаем контейнер поля - field_container = fields_locators.get(field_name) - - if not field_container: - logger.warning(f"Field '{field_name}' not found in form. Available fields: {list(fields_locators.keys())}") - return - - # Находим input внутри контейнера - input_field = field_container.locator("input").first - - if input_field.count() == 0: - logger.warning(f"Input element not found in container for field '{field_name}'") - return - - # Проверяем видимость - if not input_field.is_visible(): - logger.debug(f"Field '{field_name}' is not visible, scrolling into view...") - input_field.scroll_into_view_if_needed() - self.wait_for_timeout(300) - - # Проверяем, не disabled ли поле - is_disabled = input_field.get_attribute("disabled") - is_readonly = input_field.get_attribute("readonly") - - if is_disabled or is_readonly: - logger.warning(f"Field '{field_name}' is disabled or readonly") - return - - # Очищаем поле - input_field.click() - input_field.press("Control+A") - input_field.press("Backspace") - - # Заполняем значение - input_field.fill(value) - logger.debug(f"Filled '{field_name}': {value}") - - # Обязательные поля - if rack_data.name: - clear_and_fill("Имя", rack_data.name) - - # Опциональные поля - if rack_data.serial: - clear_and_fill("Серийный номер", rack_data.serial) - - if rack_data.inventory: - clear_and_fill("Инвентарный номер", rack_data.inventory) - - if rack_data.comment: - clear_and_fill("Комментарий", rack_data.comment) - - logger.debug("Text fields filled successfully") - - def _get_form_fields(self) -> dict: - """ - Получает все поля формы стойки. - - Returns: - dict: Словарь {название поля: Locator контейнера поля} - - Raises: - ValueError: Если контейнер формы не найден - """ - - # Получаем контейнер формы (второй элемент) - container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1) - - if container_locator.count() == 0: - logger.error("Form container not found") - raise ValueError("Form container not found") - - return self.get_input_fields_locators(container_locator) - - def _scroll_until_element(self, locator: Locator, name: str) -> None: - """ - Скроллит список до тех пор, пока не перестанут подгружаться новые элементы. - - Args: - locator (Locator): Локатор элементов или строка с CSS/XPath - name (str): Имя элемента для поиска - """ - - loc = self.get_locator(locator) - - items_count = 0 - attempts = 0 - max_attempts = 3 - last_item_name = "" - - while attempts < max_attempts: - self.page.wait_for_timeout(300) - - item_texts = loc.all_inner_texts() - item_names = item_texts[0].splitlines() - current_count = len(item_names) - - if current_count == items_count: - attempts += 1 - else: - items_count = current_count - attempts = 0 - - if name in item_names: - last_item_name = name - else: - last_item_name = item_names[current_count-1] - element = loc.get_by_role("listitem").filter( - has_text=last_item_name - ) - element.scroll_into_view_if_needed() - - self.wait_for_timeout(300) - - def fill_rack_data(self, rack_data: RackData) -> None: - """ - Заполняет данные для создания стойки. - - Args: - rack_data (RackData): Данные стойки - """ - - logger.debug(f"Filling rack data: {rack_data.name}") - - self._fill_text_fields(rack_data) - self._fill_combobox_fields(rack_data) - - logger.debug("Rack data filled successfully") - - # Проверки: - - def check_rack_fields_presence(self) -> None: - """ - Проверяет наличие полей специфичных для стойки. - - Raises: - AssertionError: Если какое-либо поле не найдено - """ - - logger.debug("Checking rack fields presence...") - - # Получаем все поля формы - fields_locators = self._get_form_fields() - - logger.debug(f"Found fields in form: {list(fields_locators.keys())}") - - # Список ожидаемых полей для стойки - expected_fields = [ - "Имя", - "Высота в юнитах", - "Глубина (мм)", - "Серийный номер", - "Инвентарный номер", - "Комментарий", - "Ввод кабеля", - "Состояние", - "Владелец", - "Обслуживающая организация", - "Проект/Титул" - ] - - # Проверяем наличие обязательных полей с помощью assert - required_fields = ["Имя", "Высота в юнитах", "Глубина (мм)"] - - for field_name in required_fields: - # Проверяем наличие поля в словаре - assert field_name in fields_locators, f"Required field '{field_name}' not found" - - field_container = fields_locators[field_name] - # check_visibility внутри использует expect, который тоже вызывает AssertionError - self.check_visibility(field_container, f"Required field '{field_name}' not visible") - logger.debug(f"Required field '{field_name}' found and visible") - - # Проверяем наличие дополнительных полей (только логгирование) - for field_name in expected_fields: - if field_name in fields_locators: - field_container = fields_locators[field_name] - if field_container.is_visible(): - logger.debug(f"Optional field '{field_name}' found and visible") - else: - logger.debug(f"Optional field '{field_name}' found but not visible") - else: - logger.debug(f"Optional field '{field_name}' not found in form") - - logger.debug("All main rack fields are present") diff --git a/forms/__pycache__/create_rack_form.cpython-313.pyc b/forms/__pycache__/create_rack_form.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5d853a9dd0eb6f268c37152be7c5334936b9088 GIT binary patch literal 24222 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 tdCGGwcZn;7hJJ>9w;Ta&%|?_}}u;EbagR literal 0 HcmV?d00001 diff --git a/forms/__pycache__/edit_rack_form.cpython-313.pyc b/forms/__pycache__/edit_rack_form.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df284f3a2b04fb1e6a64b1b22b647dba7d63232c GIT binary patch literal 29736 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 PmV&DtzvSr6n(}`DI-RTK literal 0 HcmV?d00001 diff --git a/forms/create_rack_form.py b/forms/create_rack_form.py new file mode 100644 index 0000000..d1a3fef --- /dev/null +++ b/forms/create_rack_form.py @@ -0,0 +1,520 @@ +"""Модуль для работы с формой создания стойки.""" + +import time +from dataclasses import dataclass +from typing import List, Dict, Any +from playwright.sync_api import Page +from tools.logger import get_logger +from locators.rack_locators import RackLocators +from elements.text_input_element import TextInput +from components.base_component import BaseComponent +from components.dropdown_list_component import DropdownList + + +logger = get_logger("CREATE_RACK_FORM") +logger.setLevel("INFO") + + +@dataclass +class CreateRackData: + """Класс для хранения данных создаваемой стойки.""" + + # Основные поля + 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): + """Компонент для работы с формой создания стойки.""" + + # Маппинг текстовых полей + TEXT_FIELDS_MAPPING = { + "Имя": ("name", "name_input"), + "Комментарий": ("comment", "comment_input"), + "Серийный номер": ("serial", "serial_input"), + "Инвентарный номер": ("inventory", "inventory_input"), + } + + # Маппинг полей для заполнения combobox полей + COMBOBOX_FIELDS_MAPPING = { + "Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"), + "Состояние": ("state", "state_input", "state_list"), + "Высота в юнитах": ("usize", "usize_input", "usize_list"), + "Глубина (мм)": ("depth", "depth_input", "depth_list"), + "Владелец": ("owner", "owner_input", "owner_list"), + "Обслуживающая организация": ("service_org", "service_input", "service_list"), + "Проект/Титул": ("project", "project_input", "project_list") + } + + # Локаторы для текстовых полей (из RackLocators) + TEXT_FIELDS_LOCATORS = { + "Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME, + "Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT, + "Серийный номер": RackLocators.CREATE_RACK_FORM_FIELD_SERIAL, + "Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY, + } + + # Локаторы для combobox полей (из RackLocators) + COMBOBOX_FIELDS_LOCATORS = { + "Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE, + "Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH, + "Ввод кабеля": RackLocators.CREATE_RACK_FORM_SELECT_CABLE_INPUT, + "Состояние": RackLocators.CREATE_RACK_FORM_SELECT_CONDITION_TYPE, + "Владелец": RackLocators.CREATE_RACK_FORM_SELECT_OWNER, + "Обслуживающая организация": RackLocators.CREATE_RACK_FORM_SELECT_SERVICE_PROVIDER, + "Проект/Титул": RackLocators.CREATE_RACK_FORM_SELECT_PROJECT, + } + + def __init__(self, page: Page) -> None: + """Инициализирует компонент формы создания стойки. + + 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]: + """Заполняет поля формы создания стойки. + + Args: + rack_data: Данные для заполнения + + Returns: + Словарь с результатами заполнения + """ + + results = { + "text_fields_filled": 0, + "combobox_fields_filled": 0, + } + + self._fill_text_fields(rack_data, results) + self._fill_combobox_fields(rack_data, results) + + logger.info(f"Filled {results['text_fields_filled']} text fields and " + f"{results['combobox_fields_filled']} combobox fields") + 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 diff --git a/forms/edit_rack_form.py b/forms/edit_rack_form.py new file mode 100644 index 0000000..e0e03db --- /dev/null +++ b/forms/edit_rack_form.py @@ -0,0 +1,649 @@ +"""Модуль для работы с формой редактирования стойки в модальном окне.""" + +import time +from dataclasses import dataclass +from typing import Optional, List, Dict, Any +from playwright.sync_api import Page +from tools.logger import get_logger +from locators.rack_locators import RackLocators +from elements.text_input_element import TextInput +from components.base_component import BaseComponent +from components.dropdown_list_component import DropdownList + + +logger = get_logger("EDIT_RACK_FORM") +logger.setLevel("INFO") + + +@dataclass +class EditRackFormData: + """Класс для хранения данных редактируемой стойки.""" + + # Основные поля + name: str = "" + serial: str = "" + inventory: str = "" + comment: 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 + + +class EditRackForm(BaseComponent): + """Компонент для работы с формой редактирования стойки в модальном окне.""" + + # Маппинг текстовых полей + TEXT_FIELDS_MAPPING = { + "Имя": ("name", "name_input"), + "Комментарий": ("comment", "comment_input"), + "Серийный номер": ("serial", "serial_input"), + "Инвентарный номер": ("inventory", "inventory_input"), + "Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"), + } + + # Маппинг полей для заполнения combobox полей + COMBOBOX_FIELDS_MAPPING = { + "Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"), + "Состояние": ("state", "state_input", "state_list"), + "Глубина (мм)": ("depth", "depth_input", "depth_list"), + "Высота в юнитах": ("usize", "usize_input", "usize_list"), + "Владелец": ("owner", "owner_input", "owner_list"), + "Обслуживающая организация": ("service_org", "service_input", "service_list"), + "Проект/Титул": ("project", "project_input", "project_list") + } + + # Локаторы для текстовых полей (из RackLocators) + TEXT_FIELDS_LOCATORS = { + "Имя": RackLocators.EDIT_RACK_FORM_FIELD_NAME, + "Комментарий": RackLocators.EDIT_RACK_FORM_FIELD_COMMENT, + "Серийный номер": RackLocators.EDIT_RACK_FORM_FIELD_SERIAL, + "Инвентарный номер": RackLocators.EDIT_RACK_FORM_FIELD_INVENTORY, + "Выделенная мощность (Вт/ВА)": RackLocators.EDIT_RACK_FORM_FIELD_POWER, + } + + # Локаторы для combobox полей (из RackLocators) + COMBOBOX_FIELDS_LOCATORS = { + "Ввод кабеля": RackLocators.EDIT_RACK_FORM_SELECT_CABLE_INPUT, + "Состояние": RackLocators.EDIT_RACK_FORM_SELECT_CONDITION_TYPE, + "Глубина (мм)": RackLocators.EDIT_RACK_FORM_SELECT_DEPTH, + "Высота в юнитах": RackLocators.EDIT_RACK_FORM_SELECT_USIZE, + "Владелец": RackLocators.EDIT_RACK_FORM_SELECT_OWNER, + "Обслуживающая организация": RackLocators.EDIT_RACK_FORM_SELECT_SERVICE_PROVIDER, + "Проект/Титул": RackLocators.EDIT_RACK_FORM_SELECT_PROJECT, + } + + # Локатор для чекбокса вентиляционной панели + CHECKBOX_VENTILATION = RackLocators.EDIT_RACK_FORM_CHECKBOX_VENTILATION + + def __init__(self, page: Page) -> None: + """Инициализирует компонент формы редактирования стойки. + + 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.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 = { + "text_fields_filled": 0, + "combobox_fields_filled": 0, + "checkboxes_set": 0 + } + + self._fill_text_fields(rack_data, results) + self._fill_combobox_fields(rack_data, results) + self._set_checkbox(rack_data, results) + + logger.info(f"Filled {results['text_fields_filled']} text fields, " + f"{results['combobox_fields_filled']} combobox fields, " + f"{results['checkboxes_set']} checkboxes") + 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 diff --git a/frames/__pycache__/create_element_frame.cpython-313.pyc b/frames/__pycache__/create_element_frame.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04dc5eee40e6d89582d49f46e3a0447bc9d2f428 GIT binary patch literal 16412 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 None: @@ -25,7 +25,6 @@ class CreateChildElementFrame(BaseComponent): Args: page (Page): Экземпляр страницы Playwright """ - super().__init__(page) # Инициализация компонентов @@ -38,7 +37,7 @@ class CreateChildElementFrame(BaseComponent): has_text="Создать дочерний элемент в" ).get_by_role("button").nth(0) - # Кнопка "Отменить" - используем рабочий локатор из старой версии + # Кнопка "Отменить" - используем рабочий локатор cancel_button_locator = self.page.get_by_role("navigation").filter( has_text=re.compile('Создать дочерний элемент в') ).get_by_role("button").nth(1) @@ -56,25 +55,20 @@ class CreateChildElementFrame(BaseComponent): Args: field_name (str): Название поля для очистки """ - logger.debug(f"Clearing combobox field '{field_name}'...") # Получаем контейнер формы - container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1) + container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER).nth(1) fields_locators = self.get_input_fields_locators(container_locator) if field_name not in fields_locators: logger.warning(f"Field '{field_name}' not found in form") return - # Получаем контейнер поля field_container = fields_locators[field_name] - - # Прокручиваем до поля field_container.scroll_into_view_if_needed() self.wait_for_timeout(300) - # Проверяем видимость if not field_container.is_visible(): logger.debug(f"Field '{field_name}' is not visible after scrolling") return @@ -82,11 +76,7 @@ class CreateChildElementFrame(BaseComponent): # Ищем кнопку закрытия (крестик) внутри контейнера поля close_button = field_container.locator("i.mdi-close").first - # Проверяем наличие и видимость кнопки закрытия if close_button.count() > 0: - logger.debug(f"Found close button for field '{field_name}'") - - # Если кнопка закрытия видима - кликаем на нее close_button.click(force=True) self.wait_for_timeout(300) logger.debug(f"Combobox field '{field_name}' cleared using close button") @@ -95,13 +85,11 @@ class CreateChildElementFrame(BaseComponent): def click_add_button(self) -> None: """Кликает на кнопку 'Добавить'.""" - logger.debug("Clicking on 'Add' button...") self.toolbar.click_button("add") def click_cancel_button(self) -> None: """Кликает на кнопку 'Отменить'.""" - logger.debug("Clicking on 'Cancel' button...") self.toolbar.click_button("cancel") @@ -112,7 +100,6 @@ class CreateChildElementFrame(BaseComponent): Returns: str: Выбранный класс объекта или пустая строка если ничего не выбрано """ - return self.selection_bar.get_selection_bar_title() def is_field_filled(self, field_name: str, container_locator: Locator = None) -> bool: @@ -126,38 +113,28 @@ class CreateChildElementFrame(BaseComponent): Returns: bool: True если поле заполнено, False в противном случае """ - logger.debug(f"Checking if field '{field_name}' is filled...") - # Если контейнер не передан, используем контейнер по умолчанию if container_locator is None: - container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1) + container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER).nth(1) - # Получаем словарь всех полей формы fields_locators = self.get_input_fields_locators(container_locator) if field_name not in fields_locators: logger.debug(f"Field '{field_name}' not found in fields_locators") return False - # Получаем контейнер поля field_container = fields_locators[field_name] if not field_container.is_visible(): logger.debug(f"Field '{field_name}' not visible") return False - # Проверяем наличие выбранного значения через v-chip (чип выбранного значения в combobox) selected_chip = field_container.locator(".v-chip").first - - # Проверяем наличие текста в поле field_text = field_container.text_content() or "" has_text = bool(field_text.strip()) - - # Проверяем наличие чипа has_chip = selected_chip.count() > 0 and selected_chip.is_visible() - # Для текстовых полей проверяем значение input if not has_chip: input_field = field_container.locator("input").first if input_field.count() > 0: @@ -167,13 +144,11 @@ class CreateChildElementFrame(BaseComponent): has_text = has_text or has_input_value logger.debug(f"Field '{field_name}' - has chip: {has_chip}, has text: {has_text}") - return has_chip or has_text def open_object_class_combobox(self) -> None: """Открывает выпадающий список combobox.""" - - container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER) + container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER) fields_locators = self.get_input_fields_locators(container_locator) combobox_container = fields_locators.get("Класс объекта учета") @@ -181,12 +156,10 @@ class CreateChildElementFrame(BaseComponent): logger.error("Combobox 'Класс объекта учета' not found") return - # Проверяем, не открыт ли уже выпадающий список menu_selector = "div.v-menu__content.menuable__content__active" is_menu_open = self.page.locator(menu_selector).count() > 0 if not is_menu_open: - # Используем OPEN_PARAMETERS_LIST_BUTTON из SelectionBarLocators open_button = combobox_container.locator(SelectionBarLocators.OPEN_PARAMETERS_LIST_BUTTON) open_button.click(force=True, timeout=5000) else: @@ -199,18 +172,10 @@ class CreateChildElementFrame(BaseComponent): Args: class_name (str): Название класса объекта для выбора """ - logger.debug(f"Selecting object class: '{class_name}'...") - - # Открываем combobox self.open_object_class_combobox() - - # Выбирает значение из списка self.selection_bar.select_value(class_name) - - # Даем время на применение выбора self.wait_for_timeout(300) - logger.debug(f"Object class '{class_name}' successfully selected") # Проверки: @@ -226,23 +191,16 @@ class CreateChildElementFrame(BaseComponent): ValueError: Если поле не найдено в форме AssertionError: Если поле не подсвечено ошибкой """ - logger.debug(f"Checking field '{field_name}' for error highlighting...") - # Получаем контейнеры всех полей - container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER) + container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER) 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, ( @@ -263,23 +221,16 @@ class CreateChildElementFrame(BaseComponent): ValueError: Если поле не найдено в форме AssertionError: Если поле подсвечено ошибкой """ - logger.debug(f"Checking field '{field_name}' for absence of error highlighting...") - # Получаем контейнеры всех полей - container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER) + container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER) 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, ( @@ -299,7 +250,6 @@ class CreateChildElementFrame(BaseComponent): Raises: AssertionError: Если выбранный класс не соответствует ожидаемому """ - logger.debug(f"Checking selected object class: '{expected_class}'...") self.wait_for_timeout(500) @@ -313,10 +263,7 @@ class CreateChildElementFrame(BaseComponent): f"Expected: '{expected_class}', Got: '{actual_class}'" ) - logger.debug( - f"Object class '{expected_class}' successfully selected " - f"(actual: '{actual_class}')" - ) + logger.debug(f"Object class '{expected_class}' successfully selected (actual: '{actual_class}')") def check_toolbar_title(self, expected_title: str) -> None: """ @@ -328,17 +275,14 @@ class CreateChildElementFrame(BaseComponent): Raises: AssertionError: Если заголовок не соответствует ожидаемому """ - logger.debug(f"Checking toolbar title: '{expected_title}'...") - # Используем метод тулбара с фильтрацией по тексту actual_text = self.toolbar.get_toolbar_title_text( filter_text="Создать дочерний элемент в" ) assert expected_title in actual_text, ( - f"Title does not match. Expected: '{expected_title}', " - f"Got: '{actual_text}'" + f"Title does not match. Expected: '{expected_title}', Got: '{actual_text}'" ) logger.debug(f"Toolbar title is correct: '{actual_text}'") @@ -347,10 +291,9 @@ class CreateChildElementFrame(BaseComponent): """ Проверяет наличие и функциональность кнопок тулбара. """ - self.toolbar.check_button_visibility("add") self.toolbar.check_button_tooltip("add", "Добавить") self.toolbar.check_button_visibility("cancel") self.toolbar.check_button_tooltip("cancel", "Отменить") self.toolbar.click_button("cancel") - self.wait_for_timeout(500) + self.wait_for_timeout(500) \ No newline at end of file diff --git a/locators/rack_locators.py b/locators/rack_locators.py index b84159a..fa96073 100644 --- a/locators/rack_locators.py +++ b/locators/rack_locators.py @@ -28,33 +28,54 @@ class RackLocators: ACTIVE_TAB = ("//div[@data-testid='CABINET_SHOW__tabs']" "//a[contains(@class, 'v-tabs__item--active')]") - # Контейнер формы создания/редактирования стойки - FORM_INPUT_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]" + # ================ ЛОКАТОРЫ ДЛЯ ФОРМЫ СОЗДАНИЯ СТОЙКИ =================== + + # Контейнер формы создания стойки + CREATE_RACK_FORM_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]" + + # Text + CREATE_RACK_FORM_FIELD_NAME = "[data-testid='create-location-bar__text-field__name']" + CREATE_RACK_FORM_FIELD_COMMENT = "[data-testid='create-location-bar__text-field__comment']" + CREATE_RACK_FORM_FIELD_SERIAL = "[data-testid='create-location-bar__text-field__serial_number']" + CREATE_RACK_FORM_FIELD_INVENTORY = "[data-testid='create-location-bar__text-field__inventory_number']" + + # Сombobox + CREATE_RACK_FORM_SELECT_USIZE = "[data-testid='create-location-bar__select__usize']" + CREATE_RACK_FORM_SELECT_DEPTH = "[data-testid='create-location-bar__select__depth']" + CREATE_RACK_FORM_SELECT_CABLE_INPUT = "[data-testid='create-location-bar__select__cable_input']" + CREATE_RACK_FORM_SELECT_CONDITION_TYPE = "[data-testid='create-location-bar__select__condition_type']" + CREATE_RACK_FORM_SELECT_OWNER = "[data-testid='create-location-bar__select__owner']" + CREATE_RACK_FORM_SELECT_SERVICE_PROVIDER = "[data-testid='create-location-bar__select__service_provider']" + CREATE_RACK_FORM_SELECT_PROJECT = "[data-testid='create-location-bar__select__project']" + + + # ================ ЛОКАТОРЫ ДЛЯ ФОРМЫ РЕДАКТИРОВАНИЯ СТОЙКИ =================== # Форма редактирования стойки в модальном окне - RACK_EDIT_FORM = "[data-testid='cabinet-bar__cabinet-form']" + EDIT_RACK_FORM = "[data-testid='cabinet-bar__cabinet-form']" - # Локаторы полей формы - INPUT_FORM_RACK_DATA = f"{RACK_EDIT_FORM}" - INPUT_FORM_RACK_DATA_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']" - INPUT_FORM_RACK_DATA_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']" - INPUT_FORM_RACK_DATA_FIELD_SERIAL = "[data-testid='cabinet-bar__main__text-field__serial_number']" - INPUT_FORM_RACK_DATA_FIELD_INVENTORY = "[data-testid='cabinet-bar__main__text-field__inventory_number']" - INPUT_FORM_RACK_DATA_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']" + # Text + EDIT_RACK_FORM_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']" + EDIT_RACK_FORM_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']" + EDIT_RACK_FORM_FIELD_SERIAL = "[data-testid='cabinet-bar__main__text-field__serial_number']" + EDIT_RACK_FORM_FIELD_INVENTORY = "[data-testid='cabinet-bar__main__text-field__inventory_number']" + EDIT_RACK_FORM_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']" - # Локаторы для combobox полей - INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY = "[data-testid='cabinet-bar__select_enum__select-field__cable_input']" - INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE = "[data-testid='cabinet-bar__select_enum__select-field__condition_type']" - INPUT_FORM_RACK_DATA_FIELD_DEPTH = "[data-testid='cabinet-bar__select_enum__select-field__depth']" - INPUT_FORM_RACK_DATA_FIELD_USIZE = "[data-testid='cabinet-bar__select_enum__select-field__usize']" - INPUT_FORM_RACK_DATA_FIELD_OWNER = "[data-testid='cabinet-bar__select__select-field__owner']" - INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER = "[data-testid='cabinet-bar__select__select-field__service_provider']" - INPUT_FORM_RACK_DATA_FIELD_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']" + # Сombobox + EDIT_RACK_FORM_SELECT_CABLE_INPUT = "[data-testid='cabinet-bar__select_enum__select-field__cable_input']" + EDIT_RACK_FORM_SELECT_CONDITION_TYPE = "[data-testid='cabinet-bar__select_enum__select-field__condition_type']" + EDIT_RACK_FORM_SELECT_DEPTH = "[data-testid='cabinet-bar__select_enum__select-field__depth']" + EDIT_RACK_FORM_SELECT_USIZE = "[data-testid='cabinet-bar__select_enum__select-field__usize']" + EDIT_RACK_FORM_SELECT_OWNER = "[data-testid='cabinet-bar__select__select-field__owner']" + EDIT_RACK_FORM_SELECT_SERVICE_PROVIDER = "[data-testid='cabinet-bar__select__select-field__service_provider']" + EDIT_RACK_FORM_SELECT_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']" - # Чекбоксы - INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']" - INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')" - INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']" + # Checkbox + EDIT_RACK_FORM_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']" + EDIT_RACK_FORM_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')" + EDIT_RACK_FORM_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']" + + # ================ ЛОКАТОРЫ ДЛЯ ВЫПАДАЮЩИХ СПИСКОВ =================== # Локаторы для меню combobox MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]" @@ -154,4 +175,4 @@ class RackLocators: SETTINGS_PUSH_RULES = "[data-testid*='rules.push']" # Кнопки вкладки "Настройки" - SETTINGS_CANCEL_BUTTON = "[data-testid='LOCATION_SETTINGS__btn__cancel']" \ No newline at end of file + SETTINGS_CANCEL_BUTTON = "[data-testid='LOCATION_SETTINGS__btn__cancel']" diff --git a/makers/__pycache__/edit_rack_maker.cpython-313.pyc b/makers/__pycache__/edit_rack_maker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee9a2028bf164d8163f016611d47cbfeec714772 GIT binary patch literal 49906 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 R^RO!|mf!wdU?Eoh{|kOaO=|!E literal 0 HcmV?d00001 diff --git a/components_derived/modal_edit_rack.py b/makers/edit_rack_maker.py similarity index 76% rename from components_derived/modal_edit_rack.py rename to makers/edit_rack_maker.py index 0d56cf1..2285057 100644 --- a/components_derived/modal_edit_rack.py +++ b/makers/edit_rack_maker.py @@ -1,63 +1,32 @@ """Модуль для работы с модальным окном редактирования стойки.""" import re -from dataclasses import dataclass from typing import Optional, List, Tuple, Any from playwright.sync_api import Page from tools.logger import get_logger from locators.rack_locators import RackLocators -from elements.text_input_element import TextInput -from elements.text_element import Text -from elements.checkbox_element import Checkbox from components.modal_window_component import ModalWindowComponent from components.dropdown_list_component import DropdownList from components.confirm_component import ConfirmComponent +from elements.text_input_element import TextInput +from elements.text_element import Text +from forms.edit_rack_form import EditRackForm, EditRackFormData -logger = get_logger("MODAL_EDIT_RACK") +logger = get_logger("EDIT_RACK_MAKER") logger.setLevel("INFO") -@dataclass -class RackEditData: - """Класс для хранения данных редактирования стойки. - Содержит все возможные поля, которые могут быть изменены - в модальном окне редактирования стойки. - """ - - # Основные поля (редактируемые) - name: str = "" - serial: str = "" - inventory: str = "" - comment: str = "" - allocated_power: str = "" - - # Combobox поля (редактируемые) - cable_entry: str = "" - state: str = "" - depth: str = "" - usize: str = "" - owner: str = "" - service_org: str = "" - project: str = "" - - # Checkbox поля (редактируемые) - ventilation_panel: Optional[bool] = None - - # Правила доступа - read_access_rules: str = "" - write_access_rules: str = "" - sms_access_rules: str = "" - email_access_rules: str = "" - push_access_rules: str = "" +# Используем EditRackFormData +EditRackData = EditRackFormData -class ModalEditRack(ModalWindowComponent): +class EditRackMaker(ModalWindowComponent): """Компонент для работы с модальным окном редактирования стойки. Предоставляет методы для взаимодействия с элементами окна: - переключение между вкладками - - заполнение полей общей информации + - заполнение полей общей информации (через EditRackForm) - работа с изображениями - настройка правил доступа - сохранение/отмена изменений @@ -69,47 +38,7 @@ class ModalEditRack(ModalWindowComponent): TAB_IMAGE = "Изображение" TAB_SETTINGS = "Настройки" - # Маппинг полей для заполнения текстовых полей - TEXT_FIELDS_MAPPING = { - "Имя": ("name", "name_input"), - "Комментарий": ("comment", "comment_input"), - "Серийный номер": ("serial", "serial_input"), - "Инвентарный номер": ("inventory", "inventory_input"), - "Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"), - } - - # Маппинг полей для заполнения combobox полей - COMBOBOX_FIELDS_MAPPING = { - "Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"), - "Состояние": ("state", "state_input", "state_list"), - "Глубина (мм)": ("depth", "depth_input", "depth_list"), - "Высота в юнитах": ("usize", "usize_input", "usize_list"), - "Владелец": ("owner", "owner_input", "owner_list"), - "Обслуживающая организация": ("service_org", "service_input", "service_list"), - "Проект/Титул": ("project", "project_input", "project_list") - } - - # Локаторы для текстовых полей (из RackLocators) - TEXT_FIELDS_LOCATORS = { - "Имя": RackLocators.INPUT_FORM_RACK_DATA_FIELD_NAME, - "Комментарий": RackLocators.INPUT_FORM_RACK_DATA_FIELD_COMMENT, - "Серийный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERIAL, - "Инвентарный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_INVENTORY, - "Выделенная мощность (Вт/ВА)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_POWER, - } - - # Локаторы для combobox полей (из RackLocators) - COMBOBOX_FIELDS_LOCATORS = { - "Ввод кабеля": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY, - "Состояние": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE, - "Глубина (мм)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_DEPTH, - "Высота в юнитах": RackLocators.INPUT_FORM_RACK_DATA_FIELD_USIZE, - "Владелец": RackLocators.INPUT_FORM_RACK_DATA_FIELD_OWNER, - "Обслуживающая организация": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER, - "Проект/Титул": RackLocators.INPUT_FORM_RACK_DATA_FIELD_PROJECT, - } - - # Маппинг полей для вкладки "Настройки" + # Маппинг полей для вкладки "Настройки" - оставляем только то, что специфично для модального окна ACCESS_RULES_MAPPING = { "Правила доступа для чтения": ( "read_access_rules", "rules_read_input", "rules_read_list" @@ -148,11 +77,11 @@ class ModalEditRack(ModalWindowComponent): super().__init__(page) self.rack_name = rack_name self.page = page - self.available_fields = None self.active_tab = self.TAB_GENERAL self.tabs = {} self.content_items = {} self.delete_confirm = None + self.edit_form = None # Настройка заголовка и кнопки закрытия self.window_title = rack_name @@ -198,101 +127,11 @@ class ModalEditRack(ModalWindowComponent): def _init_general_tab_content(self) -> None: """Инициализирует содержимое вкладки 'Общая информация'.""" - # Получаем доступные поля формы с помощью базового метода - self.available_fields = self.get_input_fields_locators( - self.page.locator(RackLocators.INPUT_FORM_RACK_DATA)) - - self._init_text_fields() - self._init_combobox_fields() - self._init_checkbox_fields() - - def _init_text_fields(self) -> None: - """Инициализирует текстовые поля формы.""" - - for field_label, (_, 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(): - field_input = TextInput(self.page, element, widget_name) - self.add_content_item(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, (_, 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(): - field_input = TextInput(self.page, element, input_name) - self.add_content_item(input_name, field_input) - self.add_content_item(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( - RackLocators.INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION - ).first - - if checkbox_input.count() == 0: - return - - checkbox = Checkbox(self.page, checkbox_input, "ventilation_panel") - self.add_content_item("ventilation_checkbox", checkbox) - - label_locator = self.page.locator("label:has-text('Вентиляционная панель')").first - if label_locator.count() > 0: - label_text = Text(self.page, label_locator, "ventilation_checkbox_label") - self.add_content_item("ventilation_checkbox_label", label_text) - - logger.debug("Initialized ventilation panel checkbox") + # Инициализируем форму редактирования + self.edit_form = EditRackForm(self.page) + # Копируем content_items из формы + self.content_items.update(self.edit_form.content_items) + logger.debug("General tab content initialized via EditRackForm") def _init_image_tab_content(self) -> None: """Инициализирует содержимое вкладки 'Изображение'.""" @@ -1020,7 +859,7 @@ class ModalEditRack(ModalWindowComponent): save_button.click() logger.debug("Clicked done button") - def fill_rack_data(self, rack_data: RackEditData) -> dict: + def fill_rack_data(self, rack_data: EditRackData) -> dict: """Заполняет поля формы редактирования стойки. Args: @@ -1033,139 +872,23 @@ class ModalEditRack(ModalWindowComponent): if self.active_tab != self.TAB_GENERAL: self.switch_to_tab(self.TAB_GENERAL) - results = { - "text_fields_filled": 0, - "combobox_fields_filled": 0, - "checkboxes_set": 0 - } - - self._fill_text_fields(rack_data, results) - self._fill_combobox_fields(rack_data, results) - self._set_checkbox(rack_data, results) - - return results - - def _fill_text_fields(self, rack_data: RackEditData, results: dict) -> 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 - ) -> 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.info(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: RackEditData, results: dict) -> 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 - ) -> None: - """Заполняет одно combobox поле. - - Args: - field_label: Метка поля. - input_name: Имя поля ввода. - list_name: Имя списка. - value: Значение для выбора. - results: Словарь с результатами. - """ - - try: - combobox_field = self.get_content_item(input_name) - if not combobox_field: - return - - combobox_field.click(force=True) - self.wait_for_timeout(500) - - dropdown_list = self.get_content_item(list_name) - if dropdown_list: - dropdown_list.click_item_with_text(value) - results["combobox_fields_filled"] += 1 - logger.info(f"Field '{field_label}' set: '{value}'") - except Exception as e: - logger.error(f"Error filling combobox '{field_label}': {e}") - - def _set_checkbox(self, rack_data: RackEditData, results: dict) -> None: - """Устанавливает чекбокс. - - Args: - rack_data: Данные для заполнения. - results: Словарь с результатами для обновления. - """ - - if rack_data.ventilation_panel is None: - return - - try: - checkbox = self.get_content_item("ventilation_checkbox") - if not checkbox: - return - - if rack_data.ventilation_panel: - checkbox.check(force=True) - else: - checkbox.uncheck(force=True) - - results["checkboxes_set"] += 1 - logger.info(f"Checkbox 'Ventilation panel' set to: {rack_data.ventilation_panel}") - except Exception as e: - logger.error(f"Error setting checkbox: {e}") + # Используем форму для заполнения данных + 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( self, - rack_data: RackEditData, + rack_data: EditRackData, skip_fields: Optional[List[str]] = None ) -> dict: """Проверяет, что все поля заполнены корректно. @@ -1193,8 +916,13 @@ class ModalEditRack(ModalWindowComponent): if skip_fields is None: skip_fields = [] + # Проверяем текстовые поля self._verify_text_fields(rack_data, skip_fields, results) + + # Проверяем combobox поля self._verify_combobox_fields(rack_data, skip_fields, results) + + # Проверяем чекбокс self._verify_checkbox(rack_data, skip_fields, results) if results["total_expected_fields"] > 0: @@ -1208,7 +936,7 @@ class ModalEditRack(ModalWindowComponent): def _verify_text_fields( self, - rack_data: RackEditData, + rack_data: EditRackData, skip_fields: List[str], results: dict ) -> None: @@ -1220,7 +948,11 @@ class ModalEditRack(ModalWindowComponent): results: Словарь с результатами для обновления. """ - for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items(): + 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(): expected_value = getattr(rack_data, attr_name, "") if not expected_value or not str(expected_value).strip(): continue @@ -1250,7 +982,7 @@ class ModalEditRack(ModalWindowComponent): """ try: - input_field = self.get_content_item(field_name) + input_field = self.edit_form.get_content_item(field_name) if not input_field: results["not_filled"] += 1 results["field_errors"].append(f"Field '{field_label}' input not found") @@ -1270,7 +1002,7 @@ class ModalEditRack(ModalWindowComponent): def _verify_combobox_fields( self, - rack_data: RackEditData, + rack_data: EditRackData, skip_fields: List[str], results: dict ) -> None: @@ -1282,7 +1014,11 @@ class ModalEditRack(ModalWindowComponent): results: Словарь с результатами для обновления. """ - for field_label, (attr_name, _, _) in self.COMBOBOX_FIELDS_MAPPING.items(): + 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(): expected_value = getattr(rack_data, attr_name, "") if not expected_value or not str(expected_value).strip(): continue @@ -1335,8 +1071,12 @@ class ModalEditRack(ModalWindowComponent): Значение поля или пустая строка. """ + if not self.edit_form: + return "" + actual_value = "" - locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label) + # Используем локаторы из edit_form + locator = self.edit_form.COMBOBOX_FIELDS_LOCATORS.get(field_label) if not locator: return actual_value @@ -1357,7 +1097,7 @@ class ModalEditRack(ModalWindowComponent): def _verify_checkbox( self, - rack_data: RackEditData, + rack_data: EditRackData, skip_fields: List[str], results: dict ) -> None: @@ -1379,7 +1119,7 @@ class ModalEditRack(ModalWindowComponent): return try: - checkbox = self.get_content_item("ventilation_checkbox") + checkbox = self.edit_form.get_content_item("ventilation_checkbox") if not checkbox: results["not_filled"] += 1 results["field_errors"].append("Checkbox 'Ventilation panel' not found") diff --git a/pages/create_elements_tab/create_child_element_tab.py b/pages/create_elements_tab/create_child_element_tab.py deleted file mode 100644 index 178a9bc..0000000 --- a/pages/create_elements_tab/create_child_element_tab.py +++ /dev/null @@ -1,355 +0,0 @@ -"""Модуль страницы создания дочернего элемента. - -Содержит класс для работы с формой создания дочернего элемента. -""" - -from playwright.sync_api import Page, expect -from elements.tooltip_button_element import TooltipButton -from components.toolbar_component import ToolbarComponent -from components.dropdown_list_component import DropdownList -from pages.base_page import BasePage -from tools.logger import get_logger - -logger = get_logger("CREATE_CHILD_ELEMENT") - -# =============== Локаторы ================================================ -PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i" -TOOLBAR_CONTENT = "//div[@class='v-toolbar__content']" -CREATE_BUTTON_ANCESTOR_DIV3 = "xpath=/ancestor::div[3]//button" -PANEL_HEADER_ANCESTOR_DIV2 = "xpath=/ancestor::div[2]" - -CREATE_CHILD_TITLE = "//div[contains(@class, 'v-toolbar__title') and contains(., 'Создать дочерний элемент в')]" -OBJECT_CLASS_COMBOBOX = "//div[@role='combobox' and .//label[text()='Класс объекта учета']]" -CANCEL_BUTTON = "//div[contains(@class, 'v-toolbar__title') and contains(., 'Создать дочерний элемент в')]/..//button[contains(@class, 'v-btn--icon')]" - -# Локаторы для работы с combobox -COMBOBOX_LABEL = "label" -COMBOBOX_INPUT = "input[name='entity']" -COMBOBOX_ICON = ".v-input__icon--append" -COMBOBOX_ICON_ARROW = ".v-input__icon--append .mdi-menu-down" - -# Локаторы для выпадающего списка combobox - уточненные -LISTBOX_SELECTOR = "//div[contains(@class, 'v-menu__content')]//div[@role='list']" -OPTIONS_SELECTOR = "//div[contains(@class, 'v-menu__content')]//div[@role='listitem']//span" - -# Локаторы для получения выбранного значения -SELECTED_VALUE_SPAN = "span" -#======================================================================================================== - - -class CreateChildElementTab(BasePage): - """Класс для работы с формой создания дочернего элемента.""" - - def __init__(self, page: Page) -> None: - """ - Инициализирует объект формы создания дочернего элемента. - - Args: - page: Экземпляр страницы Playwright - """ - super().__init__(page) - - # Локаторы для кнопок - panel_header_locator = self.page.locator(PANEL_HEADER) - - # Кнопка "Создать" - первая кнопка в тулбаре - create_button_locator = panel_header_locator.locator(CREATE_BUTTON_ANCESTOR_DIV3).nth(0) - - # Кнопка "Отменить" - ищем глобально на странице - cancel_button_locator = self.page.locator(CANCEL_BUTTON) - - # Инициализация кнопок - self.create_button = TooltipButton(page, create_button_locator, "add") - self.cancel_button = TooltipButton(page, cancel_button_locator, "cancel") - - # Инициализация тулбара с обеими кнопками - self.toolbar = ToolbarComponent(page, "") - self.toolbar.add_tooltip_button(create_button_locator, "add") - self.toolbar.add_tooltip_button(cancel_button_locator, "cancel") - - # Инициализация компонента выпадающего списка - self.dropdown = DropdownList(page) - - def get_toolbar_title(self) -> list[str]: - """ - Получает заголовок панели инструментов. - - Returns: - list[str]: Список элементов заголовка панели инструментов - """ - toolbar_title_locator = self.page.locator(PANEL_HEADER).\ - locator(PANEL_HEADER_ANCESTOR_DIV2).get_by_role("navigation").\ - locator(TOOLBAR_CONTENT) - - return self.toolbar.get_toolbar_composite_title_text(toolbar_title_locator) - - def should_be_toolbar_buttons(self) -> None: - """ - Проверяет наличие и функциональность кнопок тулбара. - - Raises: - AssertionError: Если кнопки недоступны или подсказки неверны. - """ - - self.wait_for_timeout(2000) - - self.toolbar.check_button_visibility("cancel") - self.toolbar.check_button_tooltip("cancel", "Отменить") - self.toolbar.get_button_by_name("cancel").click() - self.wait_for_timeout(2000) - - def click_create_button(self) -> None: - """ - Кликает на кнопку 'Создать'. - """ - logger.info("Клик на кнопку 'Создать'...") - self.toolbar.get_button_by_name("add").click() - - def click_cancel_button(self) -> None: - """ - Кликает на кнопку 'Отменить'. - """ - logger.info("Клик на кнопку 'Отменить'...") - self.toolbar.get_button_by_name("cancel").click() - - def check_toolbar_title(self, expected_title: str) -> None: - """ - Проверяет заголовок тулбара. - - Args: - expected_title: Ожидаемый заголовок тулбара - - Raises: - AssertionError: Если заголовок не соответствует ожидаемому - """ - # Используем метод тулбара с нашим специфичным локатором - self.toolbar.check_toolbar_presence_by_locator(CREATE_CHILD_TITLE, - f"Заголовок тулбара '{expected_title}' не найден") - - # Получаем текст и проверяем его - actual_text = self.toolbar.get_toolbar_title_text(CREATE_CHILD_TITLE) - assert expected_title in actual_text, f"Заголовок не совпадает. Ожидалось: '{expected_title}', Получено: '{actual_text}'" - - logger.info(f"Заголовок тулбара корректен: '{actual_text}'") - - def check_object_class_combobox_presence(self) -> None: - """ - Проверяет наличие combobox 'Класс объекта учета'. - - Raises: - AssertionError: Если combobox не найден - """ - logger.info("Проверка наличия combobox 'Класс объекта учета'...") - - combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX) - expect(combobox_locator).to_be_visible() - - logger.info("Combobox 'Класс объекта учета' найден") - - def check_object_class_combobox_content(self) -> None: - """ - Проверяет содержимое combobox 'Класс объекта учета'. - - Raises: - AssertionError: Если содержимое не соответствует ожидаемому - """ - logger.info("Проверка содержимого combobox 'Класс объекта учета'...") - - combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX) - - # Проверяем что combobox видим - expect(combobox_locator).to_be_visible() - - # Проверяем наличие label - label_locator = combobox_locator.locator(COMBOBOX_LABEL) - expect(label_locator).to_have_text("Класс объекта учета") - - # Проверяем наличие input поля - input_locator = combobox_locator.locator(COMBOBOX_INPUT) - expect(input_locator).to_be_visible() - - # Для combobox нормально иметь readonly атрибут - это стандартное поведение - # Проверяем что поле доступно для выбора (не disabled) - expect(input_locator).not_to_have_attribute("disabled", "disabled") - - # Проверяем наличие иконки стрелки - icon_locator = combobox_locator.locator(COMBOBOX_ICON_ARROW) - expect(icon_locator).to_be_visible() - - logger.info("Содержимое combobox 'Класс объекта учета' корректно") - - def open_object_class_combobox(self) -> None: - """ - Открывает выпадающий список combobox 'Класс объекта учета'. - """ - logger.info("Открытие combobox 'Класс объекта учета'...") - - combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX) - listbox_locator = self.page.locator(LISTBOX_SELECTOR) - icon_locator = combobox_locator.locator(COMBOBOX_ICON) - - # Проверяем, не открыт ли уже список - listbox_already_open = False - listbox_count = listbox_locator.count() - - if listbox_count > 0: - listbox_already_open = listbox_locator.first.is_visible() - - if not listbox_already_open: - # Только если список не открыт, кликаем на иконку - icon_locator.click(timeout=10000) - logger.info("Клик на иконку combobox выполнен") - self.wait_for_timeout(1000) - - # Проверяем что список открылся - listbox_count_after = listbox_locator.count() - listbox_visible = False - - if listbox_count_after > 0: - listbox_visible = listbox_locator.first.is_visible() - - if listbox_visible: - logger.info("Выпадающий список найден и открыт") - else: - logger.warning("Не удалось открыть выпадающий список") - - def get_object_class_options(self) -> list[str]: - """ - Получает список доступных опций из combobox. - - Returns: - list[str]: Список доступных классов объектов - """ - logger.info("Получение списка опций combobox 'Класс объекта учета'...") - - # Открываем combobox (если еще не открыт) - self.open_object_class_combobox() - - # Используем метод get_item_names из DropdownList - options_list = self.dropdown.get_item_names(LISTBOX_SELECTOR) - - # Закрываем combobox (кликаем вне его) - self.page.mouse.click(10, 10) - self.wait_for_timeout(500) - - logger.info(f"Найдено опций: {len(options_list)} - {options_list}") - return options_list - - def select_object_class(self, class_name: str) -> None: - """ - Выбирает класс объекта из выпадающего списка. - - Args: - class_name: Название класса объекта для выбора - - Raises: - AssertionError: Если класс не найден в списке - """ - logger.info(f"Выбор класса объекта: '{class_name}'...") - - # Открываем combobox - self.open_object_class_combobox() - - self.dropdown.click_item_with_text(class_name) - - # Проверяем что выбор произошел - self.wait_for_timeout(1000) - selected_value = self.get_selected_object_class() - - if class_name.lower() not in selected_value.lower() and selected_value.lower() not in class_name.lower(): - # Если выбор не произошел, получаем доступные опции для отладки - available_options = self.get_object_class_options() - logger.warning(f"Класс '{class_name}' не выбран. Текущее значение: '{selected_value}'. Доступные опции: {available_options}") - raise AssertionError(f"Не удалось выбрать класс объекта '{class_name}'") - - logger.info(f"Класс объекта '{class_name}' успешно выбран") - - def get_selected_object_class(self) -> str: - """ - Получает выбранный класс объекта учета. - - Returns: - str: Выбранный класс объекта или пустая строка если ничего не выбрано - """ - combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX) - - selected_value = "" - - # Ищем в span элементах - span_locator = combobox_locator.locator(SELECTED_VALUE_SPAN) - if span_locator.count() > 0: - for i in range(span_locator.count()): - span_text = span_locator.nth(i).text_content().strip() - if span_text and span_text not in ["Класс объекта учета"]: - selected_value = span_text - break - - logger.info(f"Выбранный класс объекта: '{selected_value}'") - return selected_value - - def check_object_class_selected(self, expected_class: str) -> None: - """ - Проверяет что выбран указанный класс объекта. - - Args: - expected_class: Ожидаемый выбранный класс объекта - - Raises: - AssertionError: Если выбранный класс не соответствует ожидаемому - """ - logger.info(f"Проверка выбранного класса объекта: '{expected_class}'...") - - # Даем время на обновление значения - self.wait_for_timeout(1000) - - actual_class = self.get_selected_object_class() - - # Проверка - допускаем частичное совпадение - if expected_class.lower() in actual_class.lower() or actual_class.lower() in expected_class.lower(): - logger.info(f"Класс объекта '{expected_class}' успешно выбран (фактически: '{actual_class}')") - else: - raise AssertionError(f"Выбранный класс не соответствует ожидаемому. Ожидалось: '{expected_class}', Получено: '{actual_class}'") - - def check_object_class_options_content(self, expected_options: list = None) -> None: - """ - Проверяет содержимое списка опций combobox. - - Args: - expected_options: Ожидаемый список опций. Если None, проверяет только что список не пустой. - - Raises: - AssertionError: Если список опций не соответствует ожидаемому - """ - logger.info("Проверка содержимого списка опций combobox...") - - # Получаем доступные опции - available_options = self.get_object_class_options() - - if expected_options is not None: - # Проверяем соответствие ожидаемому списку - assert set(available_options) == set(expected_options), ( - f"Список опций не соответствует ожидаемому. " - f"Ожидалось: {expected_options}, Получено: {available_options}" - ) - else: - # Проверяем что список не пустой - assert len(available_options) > 0, "Список опций combobox пустой" - - logger.info(f"Содержимое списка опций корректно: {available_options}") - - def check_dropdown_item_presence(self, item_text: str) -> None: - """ - Проверяет наличие элемента в выпадающем списке. - - Args: - item_text: Текст элемента для проверки - """ - logger.info(f"Проверка наличия элемента '{item_text}' в выпадающем списке...") - - # Получаем все опции и проверяем наличие - available_options = self.get_object_class_options() - - if item_text not in available_options: - raise AssertionError(f"Элемент '{item_text}' не найден в списке опций. Доступные опции: {available_options}") - - logger.info(f"Элемент '{item_text}' присутствует в списке") diff --git a/pages/create_elements_tab/create_rack_element_tab.py b/pages/create_elements_tab/create_rack_element_tab.py deleted file mode 100644 index 5965815..0000000 --- a/pages/create_elements_tab/create_rack_element_tab.py +++ /dev/null @@ -1,678 +0,0 @@ -"""Модуль страницы создания дочернего элемента. - -Содержит класс для работы с формой создания дочернего элемента. -""" -import re -from playwright.sync_api import Page, expect - -from elements.tooltip_button_element import TooltipButton -from components.toolbar_component import ToolbarComponent -from components_derived.selection_bar_component import SelectionBarComponent -from pages.main_page import MainPage -from pages.base_page import BasePage -from components.base_component import BaseComponent -from components.alert_component import AlertComponent -from components.navbar_component import NavigationPanelComponent -from locators.navigation_panel_locators import NavigationPanelLocators -from locators.combobox_locators import ComboboxLocators -from locators.rack_locators import RackLocators -from locators.alert_locators import AlertLocators -from tools.logger import get_logger - -logger = get_logger("CREATE_RACK_ELEMENT") - - -# Словарь для сопоставления названий полей с локаторами -COMBOBOX_FIELDS_MAP = { - # Обязательные поля - "Имя": RackLocators.RACK_NAME_FIELD, - "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, - "Глубина (мм)": RackLocators.RACK_DEPTH_FIELD, - - # Дополнительные текстовые поля - "Серийный номер": RackLocators.RACK_SERIAL_FIELD, - "Инвентарный номер": RackLocators.RACK_INVENTORY_FIELD, - "Комментарий": RackLocators.RACK_COMMENT_FIELD, - - # Combobox поля - "Ввод кабеля": RackLocators.RACK_CABLE_ENTRY_FIELD, - "Состояние": RackLocators.RACK_STATE_FIELD, - "Владелец": RackLocators.RACK_OWNER_FIELD, - "Обслуживающая организация": RackLocators.RACK_SERVICE_ORG_FIELD, - "Проект/Титул": RackLocators.RACK_PROJECT_FIELD -} - - -class CreateRackElementTab(BasePage): - """Класс для работы с формой создания дочернего элемента.""" - - def __init__(self, page: Page) -> None: - """ - Инициализирует объект формы создания дочернего элемента. - - Args: - page: Экземпляр страницы Playwright - """ - super().__init__(page) - - # Инициализация BaseComponent - self.base_component = BaseComponent(page) - - # Инициализация AlertComponent - self.alert = AlertComponent(page) - - # Инициализация MainPage для работы с навигацией - self.main_page = MainPage(page) - - # Инициализация NavigationPanelComponent - self.navigation_panel = NavigationPanelComponent(page) - - # Кнопка "Добавить" - первая кнопка в тулбаре - create_button_locator = self.page.get_by_role("navigation").filter(has_text=re.compile('Создать дочерний элемент в')).get_by_role("button").nth(0) - - # Кнопка "Отменить" - вторая кнопка в тулбаре - cancel_button_locator = self.page.get_by_role("navigation").filter(has_text=re.compile('Создать дочерний элемент в')).get_by_role("button").nth(1) - - # Инициализация кнопок - self.create_button = TooltipButton(page, create_button_locator, "add") - self.cancel_button = TooltipButton(page, cancel_button_locator, "cancel") - - # Инициализация тулбара с обеими кнопками - self.toolbar = ToolbarComponent(page, "Создать дочерний элемент в") - self.toolbar.add_tooltip_button(create_button_locator, "add") - self.toolbar.add_tooltip_button(cancel_button_locator, "cancel") - - # Инициализация компонента панели выбора значения для работы с combobox - self.selection_bar = SelectionBarComponent(page, ComboboxLocators.OBJECT_CLASS_COMBOBOX) - - # =============== МЕТОДЫ ДЕЙСТВИЙ ======================== - - def click_add_button(self) -> None: - """ - Кликает на кнопку 'Добавить'. - """ - self.toolbar.click_button("add") - - def click_cancel_button(self) -> None: - """ - Кликает на кнопку 'Отменить'. - """ - self.toolbar.click_button("cancel") - - def open_object_class_combobox(self) -> None: - """ - Открывает выпадающий список combobox 'Класс объекта учета'. - """ - logger.info("Открытие combobox 'Класс объекта учета'...") - self.selection_bar.open_values_list() - - def select_object_class(self, class_name: str) -> None: - """ - Выбирает класс объекта из выпадающего списка. - - Args: - class_name: Название класса объекта для выбора - - Raises: - AssertionError: Если класс не найден в списке - """ - logger.info(f"Выбор класса объекта: '{class_name}'...") - - # Открываем combobox - self.open_object_class_combobox() - - # Выбираем значение из списка - self.selection_bar.select_value(class_name) - - # Проверяем что выбор произошел - self.wait_for_timeout(1000) - selected_value = self.get_selected_object_class() - - if class_name.lower() not in selected_value.lower() and selected_value.lower() not in class_name.lower(): - # Если выбор не произошел, получаем доступные опции для отладки - available_options = self.get_object_class_options() - logger.warning(f"Класс '{class_name}' не выбран. Текущее значение: '{selected_value}'. Доступные опции: {available_options}") - raise AssertionError(f"Не удалось выбрать класс объекта '{class_name}'") - - logger.info(f"Класс объекта '{class_name}' успешно выбран") - - def get_object_class_options(self) -> list[str]: - """ - Получает список доступных опций из combobox. - - Returns: - list[str]: Список доступных классов объектов - """ - logger.info("Получение списка опций combobox 'Класс объекта учета'...") - - available_options = self.selection_bar.get_available_options() - - logger.info(f"Доступные опции класса объекта: {available_options}") - return available_options - - def get_selected_object_class(self) -> str: - """ - Получает выбранный класс объекта учета. - - Returns: - str: Выбранный класс объекта или пустая строка если ничего не выбрано - """ - # Получаем заголовок панели выбора - return self.selection_bar.get_selection_bar_title() - - def fill_rack_data(self, name: str, height: str = "42", depth: str = "1000", - serial: str = "", inventory: str = "", comment: str = "", - cable_entry: str = "", state: str = "", owner: str = "", - service_org: str = "", project: str = "") -> None: - """ - Заполняет данные для создания стойки. - - Args: - name: Наименование стойки - height: Высота в юнитах (по умолчанию 42) - depth: Глубина в мм (по умолчанию 1000) - serial: Серийный номер - inventory: Инвентарный номер - comment: Комментарий - cable_entry: Ввод кабеля - state: Состояние - owner: Владелец - service_org: Обслуживающая организация - project: Проект/Титул - """ - logger.info(f"Заполнение данных стойки: {name}") - - # Заполняем обязательные поля - name_field = self.page.locator(RackLocators.RACK_NAME_FIELD) - name_field.fill(name) - logger.info(f"Заполнено поле 'Имя': {name}") - - self._select_combobox("Высота в юнитах", height) - logger.info(f"Выбрана высота: {height} юнитов") - - self._select_combobox("Глубина (мм)", depth) - logger.info(f"Выбрана глубина: {depth} мм") - - # Заполняем опциональные поля - if serial: - serial_field = self.page.locator(RackLocators.RACK_SERIAL_FIELD) - serial_field.fill(serial) - logger.info(f"Заполнен серийный номер: {serial}") - - if inventory: - inventory_field = self.page.locator(RackLocators.RACK_INVENTORY_FIELD) - inventory_field.fill(inventory) - logger.info(f"Заполнен инвентарный номер: {inventory}") - - if comment: - comment_field = self.page.locator(RackLocators.RACK_COMMENT_FIELD) - comment_field.fill(comment) - logger.info(f"Добавлен комментарий: {comment}") - - # Заполняем дополнительные combobox поля - if cable_entry: - self._select_combobox("Ввод кабеля", cable_entry) - logger.info(f"Выбран ввод кабеля: {cable_entry}") - - if state: - self._select_combobox("Состояние", state) - logger.info(f"Выбрано состояние: {state}") - - if owner: - self._select_combobox("Владелец", owner) - logger.info(f"Выбран владелец: {owner}") - - if service_org: - self._select_combobox("Обслуживающая организация", service_org) - logger.info(f"Выбрана обслуживающая организация: {service_org}") - - if project: - self._select_combobox("Проект/Титул", project) - logger.info(f"Выбран проект/титул: {project}") - - logger.info("Данные стойки заполнены") - - def _select_combobox(self, field_name: str, value: str) -> None: - """ - Выбор значения в combobox. - - Args: - field_name: Название поля - value: Значение для выбора - """ - logger.info(f"Выбор '{value}' в поле '{field_name}'...") - - # Получаем статический локатор из словаря - if field_name not in COMBOBOX_FIELDS_MAP: - raise ValueError(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP") - - field_locator = COMBOBOX_FIELDS_MAP[field_name] - - # Для всех полей используем first() чтобы избежать strict mode violation - field_container = self.page.locator(field_locator).first - - # Прокручиваем до поля - field_container.scroll_into_view_if_needed() - self.wait_for_timeout(500) - - # Проверяем видимость поля - self.base_component.check_visibility(field_container, f"Поле '{field_name}' не найдено") - - # Универсальный клик с force=True для всех полей - field_container.click(force=True) - self.wait_for_timeout(1000) - - # Вводим значение - self.page.keyboard.type(value) - self.wait_for_timeout(500) - self.page.keyboard.press("Enter") - - logger.info(f"Поле '{field_name}' заполнено") - - def create_rack(self, rack_name: str, **kwargs) -> None: - """ - Полный процесс создания стойки. - - Args: - rack_name: Наименование стойки - **kwargs: Дополнительные параметры стойки - """ - logger.info(f"Начало процесса создания стойки: {rack_name}") - - # Выбираем класс объекта "Стойка" - self.select_object_class("Стойка") - self.wait_for_timeout(1000) - - # Проверяем наличие полей стойки - self.check_rack_fields_presence() - - # Заполняем данные - self.fill_rack_data(rack_name, **kwargs) - - # Создаем стойку - self.click_add_button() - - logger.info(f"Процесс создания стойки '{rack_name}' завершен") - - def clear_combobox_field(self, field_name: str) -> None: - """ - Очищает значение в combobox поле с помощью кнопки закрытия (крестика). - - Args: - field_name: Название поля для очистки - """ - logger.info(f"Очистка combobox поля '{field_name}' с помощью кнопки закрытия...") - - if field_name not in COMBOBOX_FIELDS_MAP: - logger.warning(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP") - return - - field_locator = COMBOBOX_FIELDS_MAP[field_name] - - # Находим поле по локатору - field_container = self.page.locator(field_locator).first - - # Проверяем что поле видимо - if not field_container.is_visible(): - logger.info(f"Поле '{field_name}' не видимо, пропускаем очистку") - return - - # Прокручиваем до поля - field_container.scroll_into_view_if_needed() - self.wait_for_timeout(500) - - # Ищем кнопку закрытия (крестик) внутри контейнера поля - close_button = field_container.locator(ComboboxLocators.COMBOBOX_CLOSE_BUTTON) - - # Проверяем наличие и видимость кнопки закрытия - if close_button.count() > 0 and close_button.is_visible(): - # Если кнопка закрытия видима - кликаем на нее - close_button.click() - self.wait_for_timeout(500) - logger.info(f"Combobox поле '{field_name}' очищено с помощью кнопки закрытия") - else: - # Если кнопки закрытия нет, просто логируем этот факт - logger.info(f"Кнопка закрытия не найдена для поля '{field_name}', очистка не выполнена") - - def clear_rack_fields(self) -> None: - """ - Очищает все поля формы создания стойки. - """ - logger.info("Очистка всех полей формы стойки...") - - # Очищаем текстовые поля - text_fields = [ - (RackLocators.RACK_NAME_FIELD, "Имя"), - (RackLocators.RACK_SERIAL_FIELD, "Серийный номер"), - (RackLocators.RACK_INVENTORY_FIELD, "Инвентарный номер"), - (RackLocators.RACK_COMMENT_FIELD, "Комментарий") - ] - - for field_locator, field_name in text_fields: - field = self.page.locator(field_locator) - if field.count() > 0 and field.first.is_visible(): - field.fill("") - logger.info(f"Текстовое поле '{field_name}' очищено") - - # Очищаем combobox поля - combobox_fields = [ - "Высота в юнитах", - "Глубина (мм)", - "Ввод кабеля", - "Состояние", - "Владелец", - "Обслуживающая организация", - "Проект/Титул" - ] - - for field_name in combobox_fields: - self.clear_combobox_field(field_name) - - logger.info("Все поля формы стойки очищены") - - # =============== МЕТОДЫ ПРОВЕРОК ======================== - def check_rack_exists(self, rack_name: str) -> bool: - """ - Проверяет, существует ли уже стойка с указанным именем в навигационной панели. - - Args: - rack_name: Имя стойки для проверки - - Returns: - bool: True если стойка существует, False если нет - """ - logger.info(f"Проверка существования стойки с именем '{rack_name}'") - - self.main_page.click_main_navigation_panel_item("Объекты") - self.main_page.click_main_navigation_panel_item("Объекты") - self.wait_for_timeout(1000) - self.main_page.click_subpanel_item("test-zone") - self.wait_for_timeout(3000) - - nav_panel_locator = NavigationPanelLocators.TREEVIEW - - # Проверяем видимость элемента через is_visible - element = self.page.locator(nav_panel_locator).get_by_text(rack_name).first - - if element.is_visible(): - logger.info(f"Стойка с именем '{rack_name}' найдена") - return True - else: - logger.info(f"Стойки с именем '{rack_name}' не найдена") - return False - - def should_be_toolbar_buttons(self) -> None: - """ - Проверяет наличие и функциональность кнопок тулбара. - - Raises: - AssertionError: Если кнопки недоступны или подсказки неверны. - """ - - self.wait_for_timeout(2000) - - self.toolbar.check_button_visibility("add") - self.toolbar.check_button_tooltip("add", "Добавить") - - self.toolbar.check_button_visibility("cancel") - self.toolbar.check_button_tooltip("cancel", "Отменить") - self.toolbar.click_button("cancel") - self.wait_for_timeout(2000) - - def check_toolbar_title(self, expected_title: str) -> None: - """ - Проверяет заголовок тулбара. - - Args: - expected_title: Ожидаемый заголовок тулбара - - Raises: - AssertionError: Если заголовок не соответствует ожидаемому - """ - logger.info(f"Проверка заголовок тулбара: '{expected_title}'...") - - # Используем метод тулбара с фильтрацией по тексту - actual_text = self.toolbar.get_toolbar_title_text( - filter_text="Создать дочерний элемент в" - ) - assert expected_title in actual_text, f"Заголовок не совпадает. Ожидалось: '{expected_title}', Получено: '{actual_text}'" - - logger.info(f"Заголовок тулбара корректен: '{actual_text}'") - - def check_object_class_combobox_presence(self) -> None: - """ - Проверяет наличие combobox 'Класс объекта учета'. - - Raises: - AssertionError: Если combobox не найден - """ - logger.info("Проверка наличия combobox 'Класс объекта учета'...") - - self.base_component.check_visibility(ComboboxLocators.OBJECT_CLASS_COMBOBOX, "Combobox 'Класс объекта учета' не найден") - logger.info("Combobox 'Класс объекта учета' найден") - - def check_object_class_combobox_content(self) -> None: - """ - Проверяет содержимое combobox 'Класс объекта учета'. - - Raises: - AssertionError: Если содержимое не соответствует ожидаемому - """ - logger.info("Проверка содержимого combobox 'Класс объекта учета'...") - - combobox_locator = self.page.locator(ComboboxLocators.OBJECT_CLASS_COMBOBOX) - - # Проверяем что combobox видим - self.base_component.check_visibility(ComboboxLocators.OBJECT_CLASS_COMBOBOX, "Combobox 'Класс объекта учета' не виден") - - # Проверяем наличие label - label_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_LABEL) - expect(label_locator).to_have_text("Класс объекта учета") - - # Проверяем наличие input поля - input_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_INPUT) - self.base_component.check_visibility(input_locator, "Input поле combobox не найдено") - - # Для combobox нормально иметь readonly атрибут - это стандартное поведение - # Проверяем что поле доступно для выбора (не disabled) - expect(input_locator).not_to_have_attribute("disabled", "disabled") - - # Проверяем наличие иконки стрелки - icon_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_ICON_ARROW) - self.base_component.check_visibility(icon_locator, "Иконка стрелки combobox не найдена") - - logger.info("Содержимое combobox 'Класс объекта учета' корректно") - - def check_object_class_selected(self, expected_class: str) -> None: - """ - Проверяет что выбран указанный класс объекта. - - Args: - expected_class: Ожидаемый выбранный класс объекта - - Raises: - AssertionError: Если выбранный класс не соответствует ожидаемому - """ - logger.info(f"Проверка выбранного класса объекта: '{expected_class}'...") - - # Даем время на обновление значения - self.wait_for_timeout(1000) - - actual_class = self.get_selected_object_class() - - # Проверка - допускаем частичное совпадение - if expected_class.lower() in actual_class.lower() or actual_class.lower() in expected_class.lower(): - logger.info(f"Класс объекта '{expected_class}' успешно выбран (фактически: '{actual_class}')") - else: - raise AssertionError(f"Выбранный класс не соответствует ожидаемому. Ожидалось: '{expected_class}', Получено: '{actual_class}'") - - def check_object_class_options_content(self, expected_options: list = None) -> None: - """ - Проверяет содержимое списка опций combobox. - - Args: - expected_options: Ожидаемый список опций. Если None, проверяет только что список не пустой. - - Raises: - AssertionError: Если список опций не соответствует ожидаемому - """ - logger.info("Проверка содержимого списка опций combobox...") - - # Получаем доступные опции - available_options = self.get_object_class_options() - - if expected_options is not None: - # Проверяем соответствие ожидаемому списку - assert set(available_options) == set(expected_options), ( - f"Список опций не соответствует ожидаемому. " - f"Ожидалось: {expected_options}, Получено: {available_options}" - ) - else: - # Проверяем что список не пустой - assert len(available_options) > 0, "Список опций combobox пустой" - - logger.info(f"Содержимое списка опций корректно: {available_options}") - - def check_dropdown_item_presence(self, item_text: str) -> None: - """ - Проверяет наличие элемента в выпадающем списке. - - Args: - item_text: Текст элемента для проверки - """ - logger.info(f"Проверка наличия элемента '{item_text}' в выпадающем списке...") - - # Получаем все опции и проверяем наличие - available_options = self.get_object_class_options() - - if item_text not in available_options: - raise AssertionError(f"Элемент '{item_text}' не найден в списке опций. Доступные опции: {available_options}") - - logger.info(f"Элемент '{item_text}' присутствует в списке") - - def check_rack_fields_presence(self) -> None: - """ - Проверяет наличие полей специфичных для стойки. - - Raises: - AssertionError: Если какое-либо поле не найдено - """ - logger.info("Проверка наличия полей для стойки...") - - # Основные обязательные поля - required_fields = [ - (RackLocators.RACK_NAME_FIELD, "Имя"), - (RackLocators.RACK_HEIGHT_FIELD, "Высота в юнитах"), - (RackLocators.RACK_DEPTH_FIELD, "Глубина (мм)") - ] - - # Дополнительные поля - optional_fields = [ - (RackLocators.RACK_SERIAL_FIELD, "Серийный номер"), - (RackLocators.RACK_INVENTORY_FIELD, "Инвентарный номер"), - (RackLocators.RACK_COMMENT_FIELD, "Комментарий"), - (RackLocators.RACK_CABLE_ENTRY_FIELD, "Ввод кабеля"), - (RackLocators.RACK_STATE_FIELD, "Состояние"), - (RackLocators.RACK_OWNER_FIELD, "Владелец"), - (RackLocators.RACK_SERVICE_ORG_FIELD, "Обслуживающая организация"), - (RackLocators.RACK_PROJECT_FIELD, "Проект/Титул") - ] - - # Проверяем обязательные поля - for field_locator, field_name in required_fields: - self.base_component.check_visibility(field_locator, f"Обязательное поле '{field_name}' не найдено") - logger.info(f"Обязательное поле '{field_name}' найдено") - - # Проверяем дополнительные поля - for field_locator, field_name in optional_fields: - field = self.page.locator(field_locator) - if field.count() > 0 and field.first.is_visible(): - logger.info(f"Дополнительное поле '{field_name}' найдено") - else: - logger.info(f"Дополнительное поле '{field_name}' не найдено или не отображается") - - logger.info("Все основные поля для стойки присутствуют") - - def check_field_highlighted_error(self, field_name: str) -> None: - """ - Проверяет, что поле подсвечено цветом ошибки (валидация не пройдена). - - Args: - field_name: Название поля для проверки - """ - logger.info(f"Проверка подсветки поля '{field_name}' цветом ошибки...") - - # Локаторы только для обязательных полей - required_fields = { - "Имя": RackLocators.RACK_NAME_FIELD, - "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, - "Глубина (мм)": RackLocators.RACK_DEPTH_FIELD - } - - if field_name not in required_fields: - raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается") - - field_locator = required_fields[field_name] - field_element = self.page.locator(field_locator) - - # Проверяем что поле видимо - self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено") - - # Ищем родительский контейнер с использованием константы - parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first - - # Проверка классов ошибки - if parent_container.count() > 0: - error_classes = AlertLocators.ERROR_CLASSES - - is_error_highlighted = False - for error_class in error_classes: - error_element = parent_container.locator(f".{error_class}") - if error_element.count() > 0: - is_error_highlighted = True - logger.info(f"Поле '{field_name}' подсвечено ошибкой") - break - - if not is_error_highlighted: - raise AssertionError(f"Поле '{field_name}' не подсвечено цветом ошибки ") - - logger.info(f"Поле '{field_name}' корректно подсвечено цветом ошибки") - - def check_field_not_highlighted_error(self, field_name: str) -> None: - """ - Проверяет, что поле НЕ подсвечено цветом ошибки (валидация успешна). - - Args: - field_name: Название поля для проверки - """ - logger.info(f"Проверка отсутствия подсветки ошибки у поля '{field_name}'...") - - # Локаторы только для обязательных полей - required_fields = { - "Имя": RackLocators.RACK_NAME_FIELD, - "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, - "Глубина (мм)": RackLocators.RACK_DEPTH_FIELD - } - - if field_name not in required_fields: - raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается") - - field_locator = required_fields[field_name] - field_element = self.page.locator(field_locator) - - # Проверяем что поле видимо - self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено") - - # Ищем родительский контейнер с использованием константы - parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first - - # Поверка отсутствия классов ошибки - if parent_container.count() > 0: - error_classes = AlertLocators.ERROR_CLASSES - - for error_class in error_classes: - error_element = parent_container.locator(f".{error_class}") - if error_element.count() > 0: - raise AssertionError(f"Поле '{field_name}' подсвечено ошибкой") - - logger.info(f"Поле '{field_name}' корректно не подсвечено цветом ошибки") diff --git a/pages/rack_tab/rack_tab.py b/pages/rack_tab/rack_tab.py deleted file mode 100644 index 01f722a..0000000 --- a/pages/rack_tab/rack_tab.py +++ /dev/null @@ -1,466 +0,0 @@ -"""Модуль тестов вкладки 'Стойка'. - -Содержит тесты для проверки функциональности -работы со стойкой оборудования. -""" - -from playwright.sync_api import Page, expect -from elements.tooltip_button_element import TooltipButton -from components.toolbar_component import ToolbarComponent -from pages.base_page import BasePage -from locators.rack_locators import RackLocators -from tools.logger import get_logger - -logger = get_logger("RACK_TAB") - -# Специфичные локаторы оставленые в основном коде -PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i" -TOOLBAR_CONTENT = "//div[@class='v-toolbar__content']" -EDIT_BUTTON_ANCESTOR_DIV3 = "xpath=/ancestor::div[3]//button" -PANEL_HEADER_ANCESTOR_DIV2 = "xpath=/ancestor::div[2]" - - -class RackTab(BasePage): - """Класс для работы с вкладкой стойки оборудования.""" - - def __init__(self, page: Page) -> None: - """ - Инициализирует объект вкладки стойки. - - Args: - page: Экземпляр страницы Playwright - """ - super().__init__(page) - - locator_button = self.page.locator(PANEL_HEADER).\ - locator(EDIT_BUTTON_ANCESTOR_DIV3).nth(0) - self.edit_button = TooltipButton(page, locator_button, "edit") - - self.toolbar = ToolbarComponent(page, "") - self.toolbar.add_tooltip_button(locator_button, "edit") - - def wait_for_rack_loading(self, timeout: int = 15000) -> None: - """ - Ожидает загрузки интерфейса стойки. - - Args: - timeout: Время ожидания в миллисекундах (по умолчанию 15000) - - Raises: - TimeoutError: Если загрузка не завершилась в указанное время - """ - logger.info("Ожидание загрузки интерфейса стойки...") - - # Ждем появления основного контейнера - main_container = self.page.locator(RackLocators.MAIN_CONTAINER) - expect(main_container).to_be_visible(timeout=timeout) - - # Ждем появления юнитов - units = self.page.locator(RackLocators.ALL_UNITS) - expect(units).to_have_count(20, timeout=timeout) - - logger.info("Интерфейс стойки загружен") - - def get_toolbar_title(self) -> list[str]: - """ - Получает заголовок панели инструментов. - - Returns: - list[str]: Список элементов заголовка панели инструментов - """ - toolbar_title_locator = self.page.locator(PANEL_HEADER).\ - locator(PANEL_HEADER_ANCESTOR_DIV2).get_by_role("navigation").\ - locator(TOOLBAR_CONTENT) - - return self.toolbar.get_toolbar_composite_title_text(toolbar_title_locator) - - def switch_to_tab(self, tab_name: str) -> None: - """ - Переключается на указанную вкладку. - - Args: - tab_name: Название вкладки для переключения - - Raises: - AssertionError: Если вкладка не найдена или недоступна - """ - logger.info(f"Переключение на вкладку '{tab_name}'...") - - tab = self.page.locator(RackLocators.TAB_BY_NAME.format(tab_name)) - - if tab.count() == 0: - raise AssertionError(f"Вкладка '{tab_name}' не найдена") - - # Проверяем активность ДО клика - if self.is_tab_active(tab_name): - logger.info(f"Вкладка '{tab_name}' уже активна") - return - - # Находим первую видимую вкладку с нужным именем - target_tab = None - for i in range(tab.count()): - element = tab.nth(i) - if element.is_visible() and element.is_enabled(): - target_tab = element - break - - if not target_tab: - raise AssertionError(f"Не найдена видимая/доступная вкладка '{tab_name}'") - - # Кликаем на вкладку - logger.info(f"Клик на вкладку '{tab_name}'...") - target_tab.click() - - # Ждем изменения активной вкладки - self._wait_for_tab_activation(tab_name) - - # Ждем загрузки контента - self.page.wait_for_timeout(500) - - def switch_to_general_info_tab(self) -> None: - """Переключается на вкладку 'Общая информация'.""" - self.switch_to_tab("Общая информация") - - def switch_to_maintenance_tab(self) -> None: - """Переключается на вкладку 'Обслуживание'.""" - self.switch_to_tab("Обслуживание") - - def switch_to_events_tab(self) -> None: - """Переключается на вкладку 'События'.""" - self.switch_to_tab("События") - - def switch_to_services_tab(self) -> None: - """Переключается на вкладку 'Сервисы'.""" - self.switch_to_tab("Сервисы") - - def is_tab_active(self, tab_name: str) -> bool: - """ - Проверяет, активна ли указанная вкладка. - - Args: - tab_name: Название вкладки для проверки - - Returns: - bool: True если вкладка активна, False в противном случае - """ - # Метод 1: Проверяем по активному классу и тексту, метод быстый, если надо универсальный оставояем метод 2 - медленный - active_tab = self.page.locator(RackLocators.ACTIVE_TAB) - - if active_tab.count() > 0 and active_tab.first.is_visible(): - active_text = active_tab.first.text_content() - if active_text and active_text.strip() == tab_name: - logger.info(f"Вкладка '{tab_name}' активна (через класс активной вкладки)") - return True - - # Метод 2: Проверяем по классам у конкретной вкладки - tab = self.page.locator(RackLocators.TAB_BY_NAME.format(tab_name)) - - if tab.count() > 0: - for i in range(tab.count()): - element = tab.nth(i) - if element.is_visible() and element.is_enabled(): - element_class = element.get_attribute("class") or "" - is_active = any( - active_class in element_class - for active_class in RackLocators.ACTIVE_TAB_CLASSES - ) - - if is_active: - logger.info(f"Вкладка '{tab_name}' активна (классы: {element_class})") - return True - - logger.info(f"Вкладка '{tab_name}' не активна") - return False - - def get_available_tabs(self) -> list[str]: - """ - Возвращает список доступных вкладок используя DOM структуру. - - Returns: - list[str]: Список названий доступных вкладок - """ - tabs = [] - - # Используем локатор для верхних вкладок - tab_elements = self.page.locator(RackLocators.ALL_TABS) - - # Ждем появления элементов - tab_elements.first.wait_for(state="visible", timeout=5000) - - total_count = tab_elements.count() - logger.info(f"Всего найдено элементов верхних вкладок: {total_count}") - - for i in range(total_count): - element = tab_elements.nth(i) - - # Проверяем видимость и доступность элемента - if element.is_visible() and element.is_enabled(): - tab_text = element.text_content() - if tab_text: - tab_text = tab_text.strip() - if tab_text and tab_text not in tabs: - tabs.append(tab_text) - logger.info(f"Найдена верхняя вкладка: '{tab_text}'") - - logger.info(f"Найдены доступные верхние вкладки: {tabs}") - return tabs - - def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None: - """ - Ожидает активации вкладки. - - Args: - tab_name: Название вкладки для ожидания - timeout: Время ожидания в миллисекундах - - Raises: - AssertionError: Если вкладка не активирована в течение таймаута - """ - logger.info(f"Ожидание активации вкладки '{tab_name}'...") - - start_time = self.page.evaluate("Date.now()") - while self.page.evaluate("Date.now()") - start_time < timeout: - if self.is_tab_active(tab_name): - logger.info(f"Вкладка '{tab_name}' успешно активирована") - return - self.page.wait_for_timeout(100) - - raise AssertionError(f"Вкладка '{tab_name}' не активирована в течение {timeout}мс") - - def should_be_toolbar_buttons(self) -> None: - """ - Проверяет наличие и функциональность кнопок тулбара. - - Raises: - AssertionError: Если кнопки недоступны или подсказки неверны. - """ - logger.info("Проверка кнопок панели инструментов...") - - self.toolbar.check_button_visibility("edit") - self.toolbar.check_button_tooltip("edit", "Изменить") - self.toolbar.get_button_by_name("edit").click() - - def should_be_rack_sides_displayed(self) -> None: - """ - Проверка отображения и структуры сторон стойки. - - Raises: - AssertionError: Если стороны стойки не отображаются корректно - """ - logger.info("Проверка отображения и структуры сторон стойки...") - - # Ожидаем загрузки - self.wait_for_rack_loading() - - # БАЗОВАЯ ПРОВЕРКА: обе стороны отображаются - logger.info("--- Базовая проверка отображения сторон ---") - - front_side_section = self.page.locator(RackLocators.FRONT_SIDE_SECTION).first - expect(front_side_section).to_be_visible(timeout=10000) - logger.info("Секция лицевой стороны найдена") - - back_side_section = self.page.locator(RackLocators.BACK_SIDE_SECTION).first - expect(back_side_section).to_be_visible(timeout=10000) - logger.info("Секция обратной стороны найдена") - - # Проверяем заголовки - front_side_title = front_side_section.locator(RackLocators.FRONT_SIDE_TITLE) - expect(front_side_title).to_be_visible(timeout=5000), "Заголовок 'Лицевая сторона' не отображается" - logger.info("Заголовок 'Лицевая сторона' отображается") - - back_side_title = back_side_section.locator(RackLocators.BACK_SIDE_TITLE) - expect(back_side_title).to_be_visible(timeout=5000), "Заголовок 'Обратная сторона' не отображается" - logger.info("Заголовок 'Обратная сторона' отображается") - - # Проверяем позиции юнитов - unit_positions = self.page.locator(RackLocators.UNIT_POSITIONS) - total_positions = unit_positions.count() - logger.info(f"Всего позиций юнитов: {total_positions}") - assert total_positions > 0, "Не найдено позиций юнитов" - - # Детальная проверка лицевой стороны - logger.info("--- Детальная проверка лицевой стороны ---") - self._check_front_side_details(front_side_section) - - # Детальная проверка обратной стороны - logger.info("--- Детальная проверка обратной стороны ---") - self._check_back_side_details(back_side_section) - - logger.info("Все проверки сторон стойки пройдены успешно") - - def _check_front_side_details(self, front_side_section) -> None: - """ - Проверка структуры лицевой стороны стойки. - - Args: - front_side_section: Локатор секции лицевой стороны - - Raises: - AssertionError: Если структура лицевой стороны некорректна - """ - # Проверяем юниты в секции лицевой стороны - front_side_units = front_side_section.locator(RackLocators.FRONT_SIDE_UNITS) - unit_count = front_side_units.count() - logger.info(f"Найдено юнитов на лицевой стороне: {unit_count}") - assert unit_count >= 1, f"Не найдено юнитов на лицевой стороне. Ожидалось минимум 1, найдено {unit_count}" - - # Проверяем наличие устройств на лицевой стороне - front_side_devices = front_side_section.locator(RackLocators.FRONT_SIDE_DEVICES) - device_count = front_side_devices.count() - logger.info(f"Найдено физических устройств на лицевой стороне: {device_count}") - - if device_count > 0: - for i in range(device_count): - device = front_side_devices.nth(i) - device_title = device.get_attribute("title") - device_classes = device.get_attribute("class") or "" - logger.info(f" Устройство {i}: title='{device_title}', classes='{device_classes}'") - - def _check_back_side_details(self, back_side_section) -> None: - """ - Проверка структуры обратной стороны стойки. - - Args: - back_side_section: Локатор секции обратной стороны - - Raises: - AssertionError: Если структура обратной стороны некорректна - """ - # Проверяем юниты в секции обратной стороны - back_side_units = back_side_section.locator(RackLocators.BACK_SIDE_UNITS) - unit_count = back_side_units.count() - logger.info(f"Найдено юнитов на обратной стороне: {unit_count}") - assert unit_count >= 1, f"Не найдено юнитов на обратной стороне. Ожидалось минимум 1, найдено {unit_count}" - - # Проверяем наличие устройств на обратной стороне - back_side_devices = back_side_section.locator(RackLocators.BACK_SIDE_DEVICES) - device_count = back_side_devices.count() - logger.info(f"Найдено физических устройств на обратной стороне: {device_count}") - - if device_count > 0: - for i in range(device_count): - device = back_side_devices.nth(i) - device_title = device.get_attribute("title") - device_classes = device.get_attribute("class") or "" - logger.info(f" Устройство {i}: title='{device_title}', classes='{device_classes}'") - - def should_be_header_panel(self, expected_toolbar_title_items: list[str]) -> None: - """ - Проверяет наличие и корректность заголовка панели. - - Args: - expected_toolbar_title_items: Ожидаемые элементы заголовка - - Raises: - AssertionError: Если заголовок панели не соответствует ожиданиям - """ - panel_header_locator = self.page.locator(PANEL_HEADER) - expect(panel_header_locator).to_be_visible(), "Panel header 'Объекты'" - - if panel_header_locator.inner_text() != 'chevron_right': - assert False, "No separator 'chevron_right' after header 'Объекты'" - - actual_toolbar_title_items = self.get_toolbar_title() - - self.check_lists_equals(actual_toolbar_title_items, - expected_toolbar_title_items, - f"Miscomparison actual {actual_toolbar_title_items} and expected {expected_toolbar_title_items}") - - self.toolbar.check_button_visibility("edit") - - - def check_tab_switching(self) -> None: - """ - Проверяет переключение между вкладками стойки в соответствии с локаторами. - - Raises: - AssertionError: Если переключение на одну или более вкладок не удалось - """ - logger.info("Тестирование функциональности переключения вкладок стойки...") - - # Вкладки - defined_tabs = [ - "Общая информация", - "Обслуживание", - "События", - "Сервисы" - ] - - logger.info(f"Тестируемые определенные вкладки: {defined_tabs}") - - successful_switches = 0 - failed_switches = [] - - # Тестируем переключение на каждую определенную вкладку - for tab_name in defined_tabs: - logger.info(f"Тестирование переключения на вкладку '{tab_name}'...") - - # Проверяем существование локатора для этой вкладки - tab_locator = RackLocators.TAB_BY_NAME.format(tab_name) - tab_elements = self.page.locator(tab_locator) - - # Проверяем наличие элементов через count() - if tab_elements.count() == 0: - logger.warning(f"Вкладка '{tab_name}' не найдена на странице") - failed_switches.append(f"Вкладка '{tab_name}' не найдена") - continue - - # Находим видимую и доступную вкладку - target_tab = None - for i in range(tab_elements.count()): - element = tab_elements.nth(i) - # Проверки видимости и доступности - if element.is_visible() and element.is_enabled(): - target_tab = element - break - - if not target_tab: - logger.warning(f"Не найдена видимая/доступная вкладка '{tab_name}'") - failed_switches.append(f"Вкладка '{tab_name}' не видима/не доступна") - continue - - # Переключаемся на вкладку - logger.info(f"Переключение на вкладку '{tab_name}'...") - - # Проверяем активность ДО клика - if self.is_tab_active(tab_name): - logger.info(f"Вкладка '{tab_name}' уже активна") - successful_switches += 1 - continue - - # Кликаем на вкладку с таймаутом - target_tab.click(timeout=5000) - - # Ждем изменения активной вкладки - self._wait_for_tab_activation(tab_name) - - # Проверяем, что вкладка активна - if not self.is_tab_active(tab_name): - logger.warning(f"Вкладка '{tab_name}' не активна после переключения") - failed_switches.append(f"Вкладка '{tab_name}' не активна после клика") - continue - - logger.info(f"Успешно переключено на вкладку '{tab_name}'") - successful_switches += 1 - - # Небольшая пауза между переключениями для стабильности - self.page.wait_for_timeout(1000) - - # Формируем итоговый отчет - logger.info("=== РЕЗУЛЬТАТЫ ПЕРЕКЛЮЧЕНИЯ ВКЛАДОК ===") - logger.info(f"Успешных переключений: {successful_switches}/{len(defined_tabs)}") - - if failed_switches: - logger.info("Неудачные переключения:") - for failure in failed_switches: - logger.info(f" - {failure}") - - # Требуем успешного переключения на все определенные вкладки - if successful_switches < len(defined_tabs): - raise AssertionError( - f"Тест переключения вкладок не пройден. " - f"Только {successful_switches} из {len(defined_tabs)} определенных вкладок переключены успешно. " - f"Ошибки: {', '.join(failed_switches)}" - ) - - logger.info(f"Все {successful_switches} определенных вкладок успешно переключены!") diff --git a/tests/e2e/create_elements/test_create_rack_element.py b/tests/e2e/create_elements/test_create_rack_element.py deleted file mode 100644 index 64a87a1..0000000 --- a/tests/e2e/create_elements/test_create_rack_element.py +++ /dev/null @@ -1,609 +0,0 @@ -"""Тест создания дочернего элемента 'Стойка'.""" - -import pytest -from playwright.sync_api import Page -from tools.logger import get_logger -from locators.navigation_panel_locators import NavigationPanelLocators -from locators.rack_locators import RackLocators -from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData -from components_derived.frames.create_child_element_frame import CreateChildElementFrame -from pages.location_page import LocationPage -from components_derived.modal_edit_rack import ModalEditRack, RackEditData -from pages.login_page import LoginPage -from pages.main_page import MainPage -from pages.rack_page import RackPage -from components.alert_component import AlertComponent - - -logger = get_logger("CREATE_RACK_TEST") -logger.setLevel("INFO") - -# @pytest.mark.smoke -class TestCreateRackElement: - """Тест создания дочернего элемента типа 'Стойка'. - - Тесты покрывают следующие сценарии: - 1. test_create_rack_content: Проверяет содержимое формы создания стойки - 2. test_create_rack_child_element: Проверяет создание дочернего элемента типа 'Стойка' - 3. test_create_rack_with_duplicate_name: Проверяет создание стойки с дублирующимся именем - 4. test_required_fields_validation: Проверяет валидацию обязательных полей при создании стойки - """ - - # Инициализируем атрибуты - main_page: MainPage = None - location_page: LocationPage = None - - @pytest.fixture(scope="function", autouse=True) - def setup(self, browser: Page) -> None: - """Фикстура для подготовки тестового окружения. - - Args: - browser (Page): Экземпляр страницы Playwright для взаимодействия с UI - """ - - # Авторизация в системе - login_page = LoginPage(browser) - login_page.do_login() - - # Мы на главной странице - self.main_page = MainPage(browser) - self.main_page.should_be_navigation_panel() - - # Переходим к Объектам - self.main_page.click_main_navigation_panel_item("Объекты") - self.main_page.wait_for_timeout(1000) - self.main_page.click_main_navigation_panel_item("test-zone") - - # Создаем экземпляр страницы локации - self.location_page = LocationPage(browser) - - @pytest.fixture - def cleanup_racks(self, browser: Page): - """Фикстура для очистки созданных стоек.""" - # Список для хранения созданных в тесте стоек - created_racks = [] - - yield created_racks - - # После завершения теста удаляем созданные стойки - if created_racks: - logger.debug(f"Cleaning up racks: {created_racks}") - - self.main_page.wait_for_timeout(500) - self.main_page.click_subpanel_item("test-zone") - self.main_page.wait_for_timeout(1000) - - # Удаляем каждую стойку если она существует - for rack_name in created_racks: - # Проверяем существование стойки - if self._check_rack_existance(browser, rack_name): - logger.debug(f"Deleting rack '{rack_name}'...") - - # Переходим на страницу стойки для удаления - self.main_page.click_subpanel_item(rack_name, parent="test-zone") - self.main_page.wait_for_timeout(1000) - - # Удаляем стойку - self._delete_rack_from_context_menu(browser, rack_name) - - # Проверяем удаление - self.main_page.click_subpanel_item("test-zone") - self.main_page.wait_for_timeout(500) - - # Дополнительная проверка удаления - rack_still_exists = self._check_rack_existance(browser, rack_name) - if rack_still_exists: - logger.error(f"Rack '{rack_name}' still exists after deletion attempt") - - logger.debug("Racks cleanup completed") - else: - logger.debug("No racks to cleanup") - - - def _create_rack(self, browser: Page, rack_name: str) -> None: - """Создает стойку. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для создания - """ - logger.debug(f"Creating rack with name '{rack_name}'") - - # Нажимаем кнопку "Создать" на тулбаре - self.location_page.click_create_button() - - # Создаем фрейм создания дочернего элемента - create_child_frame = CreateChildElementFrame(browser) - - # Нажимаем на плашку "Класс объекта учета" - create_child_frame.open_object_class_combobox() - - # Из выпадающего меню выбираем пункт "Стойка" - create_child_frame.select_object_class("Стойка") - - # Открывается набор плашек для задания параметров стойки - rack_maker = RackObjectMaker(browser) - - # Создаем объект данных стойки - rack_data = RackData( - name=rack_name, - height="42", - depth="1000" - ) - - # Заполняем данные стойки - rack_maker.fill_rack_data(rack_data) - - # Нажимаем кнопку создания - create_child_frame.click_add_button() - - # Проверяем уведомление об успешном создании - alert = AlertComponent(browser) - expected_alert_text = f"Элемент {rack_name} создан" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) - - def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None: - """Удаляет стойку через контекстное меню. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для удаления - """ - logger.debug(f"Deleting rack '{rack_name}' from context menu...") - - # 1. Находим элемент стойки в навигационной панели - rack_element = browser.locator(NavigationPanelLocators.TREEVIEW).get_by_text(rack_name, exact=True).first - - # Прокручиваем до элемента если нужно - rack_element.scroll_into_view_if_needed() - self.main_page.wait_for_timeout(500) - - # 2. Проверяем и нажимаем кнопку "Изменить" - rack_page = RackPage(browser) - - # Проверяем видимость и тултип кнопки - rack_page.should_be_toolbar_buttons() - - # Кликаем на кнопку "Изменить" - rack_page.click_edit_button() - - self.main_page.wait_for_timeout(1000) - - # 3. Создаем экземпляр ModalRackEditRack - rack_edit = ModalEditRack(browser, rack_name) - - # Используем метод для удаления - rack_edit.click_remove_button() - self.main_page.wait_for_timeout(1000) - - # 4. Проверяем уведомление об успешном удалении - alert = AlertComponent(browser) - expected_alert_text = "Успешно удалено" - alert.check_alert_presence(expected_alert_text) - - # Получаем текст alert для логирования - alert_text = alert.get_text() - logger.debug(f"Alert text after deletion: {alert_text}") - - # Закрываем alert - alert.close_alert_by_text(expected_alert_text) - - logger.debug("Rack deletion completed") - - def _perform_required_fields_test(self, create_child_frame, rack_maker, test_data): - """Выполняет один тест валидации обязательных полей. - - Args: - create_child_frame: Фрейм создания дочернего элемента - rack_maker: Объект для работы со стойкой - test_data: Словарь с данными теста - """ - # Распаковываем данные теста - name_value = test_data["name"] - height_value = test_data["height"] - depth_value = test_data["depth"] - expected_alert_height = test_data["expected_alert_height"] - expected_alert_depth = test_data["expected_alert_depth"] - - # Получаем контейнер формы - container_locator = create_child_frame.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1) - - logger.debug(f"Available fields:\ - {list(create_child_frame.get_input_fields_locators(container_locator).keys())}") - - # Проверяем и очищаем поле "Глубина (мм)" только если оно заполнено - logger.debug("Checking field: Глубина (мм)") - if create_child_frame.is_field_filled("Глубина (мм)", container_locator): - logger.debug("Field 'Глубина (мм)' is filled, performing clearing") - create_child_frame.clear_combobox_field("Глубина (мм)") - logger.debug("Clearing completed for 'Глубина (мм)'") - else: - logger.debug("Field 'Глубина (мм)' is already empty, skipping clearing") - - # Проверяем и очищаем поле "Высота в юнитах" только если оно заполнено - logger.debug("Checking field: Высота в юнитах") - if create_child_frame.is_field_filled("Высота в юнитах", container_locator): - logger.debug("Field 'Высота в юнитах' is filled, performing clearing") - create_child_frame.clear_combobox_field("Высота в юнитах") - logger.debug("Clearing completed for 'Высота в юнитах'") - else: - logger.debug("Field 'Высота в юнитах' is already empty, skipping clearing") - - # Создаем объект данных стойки - rack_data = RackData( - name=name_value, - height=height_value, - depth=depth_value - ) - - # Заполняем данные стойки - logger.debug(f"Setting test data - Name: '{name_value}', Height: '{height_value}', Depth: '{depth_value}'") - rack_maker.fill_rack_data(rack_data) - - # Нажимаем кнопку создания - logger.debug("Submitting form for validation") - create_child_frame.click_add_button() - create_child_frame.wait_for_timeout(500) - - # Проверяем валидацию полей - logger.debug("Checking validation results") - - alert = AlertComponent(create_child_frame.page) - - # Обрабатываем alert-окна - if not height_value: - logger.debug("Expecting height validation alert") - alert.check_alert_presence(expected_alert_height) - alert.close_alert_by_text(expected_alert_height) - logger.debug("Height alert handled") - - if not depth_value: - logger.debug("Expecting depth validation alert") - alert.check_alert_presence(expected_alert_depth) - alert.close_alert_by_text(expected_alert_depth) - logger.debug("Depth alert handled") - - # Проверяем подсветку обязательных полей - if height_value: - create_child_frame.check_field_error_not_highlighted("Высота в юнитах") - logger.debug("Height field validation passed") - else: - create_child_frame.check_field_error_highlighted("Высота в юнитах") - logger.debug("Height field validation failed as expected") - - if depth_value: - create_child_frame.check_field_error_not_highlighted("Глубина (мм)") - logger.debug("Depth field validation passed") - else: - create_child_frame.check_field_error_highlighted("Глубина (мм)") - logger.debug("Depth field validation failed as expected") - - # Проверяем, что остались на странице создания - create_child_frame.check_toolbar_title('Создать дочерний элемент в') - logger.debug("Test completed successfully") - - - def test_create_rack_child_element(self, browser: Page, cleanup_racks) -> None: - """Тест создания дочернего элемента типа 'Стойка'.""" - # Нажимаем кнопку "Создать" на тулбаре - self.location_page.click_create_button() - - # Создаем фрейм создания дочернего элемента - create_child_frame = CreateChildElementFrame(browser) - - # Нажимаем на плашку "Класс объекта учета" - create_child_frame.open_object_class_combobox() - - # Из выпадающего меню выбираем пункт "Стойка" - create_child_frame.select_object_class("Стойка") - - # Открывается набор плашек для задания параметров стойки - rack_maker = RackObjectMaker(browser) - - # Создаем объект данных стойки - rack_data = RackData( - name="Test-Rack-01", - height="42", - depth="1000", - serial="TEST123456", - inventory="INV-001", - comment="Тестовая стойка для автоматизации", - state="Введен в эксплуатацию", - cable_entry="сверху" - ) - - # Сохраняем имя стойки в переменную - rack_name = rack_data.name - cleanup_racks.append(rack_name) - - # Заполняем данные стойки - rack_maker.fill_rack_data(rack_data) - - # Нажимаем кнопку "Добавить" - create_child_frame.click_add_button() - - # Проверяем уведомление об успешном создании стойки - alert = AlertComponent(browser) - expected_alert_text = f"Элемент {rack_name} создан" - alert.check_alert_presence(expected_alert_text) - - # Закрываем alert - alert.close_alert_by_text(expected_alert_text) - - # Проверяем, что стойка создана и отображается - logger.debug(f"Verifying that rack '{rack_name}' was created...") - - # Обновляем навигационную панель - self.main_page.click_main_navigation_panel_item("test-zone") - - # Проверяем существование стойки в навигационной панели - rack_exists = self._check_rack_existance(browser, rack_name) - assert rack_exists, f"Rack '{rack_name}' should be visible in navigation panel after creation" - - logger.debug(f"Rack '{rack_name}' is visible in navigation panel") - logger.debug("Test for creating 'Rack' child element completed successfully") - - def test_create_rack_content(self, browser: Page) -> None: - """Тест проверки содержимого формы создания стойки.""" - # Проверяем что кнопка "Создать" доступна - self.location_page.should_be_toolbar_buttons() - - # Нажимаем кнопку "Создать" на тулбаре - self.location_page.click_create_button() - - # Создаем фрейм создания дочернего элемента - create_child_frame = CreateChildElementFrame(browser) - - # Нажимаем на плашку "Класс объекта учета" - create_child_frame.open_object_class_combobox() - - # Из выпадающего меню выбираем пункт "Стойка" - create_child_frame.select_object_class("Стойка") - - # Открывается набор плашек для задания параметров стойки - rack_maker = RackObjectMaker(browser) - - # Проверяем заголовок формы создания - create_child_frame.check_toolbar_title('Создать дочерний элемент в') - - # Проверяем что после выбора 'Стойка' появляются специфичные поля - rack_maker.check_rack_fields_presence() - logger.debug("Rack-specific fields are displayed correctly") - - create_child_frame.should_be_toolbar_buttons() - - def test_create_rack_with_duplicate_name(self, browser: Page, cleanup_racks) -> None: - """Тест создания стойки с уже существующим именем. - - Проверяет, что система корректно обрабатывает попытку создания - стойки с именем, которое уже используется. - """ - logger.debug("Starting test for creating rack with duplicate name") - - rack_name = "Test-Rack-Duplicate" - - # Проверяем, существует ли уже стойка с таким именем - if not self._check_rack_existance(browser, rack_name): - logger.debug(f"Rack with name '{rack_name}' not found. Creating first rack.") - self._create_rack(browser, rack_name) - logger.debug(f"First rack with name '{rack_name}' created successfully") - # Добавляем стойку в список для очистки - cleanup_racks.append(rack_name) - else: - logger.debug(f"Rack with name '{rack_name}' already exists, proceeding to create second one") - - # Создаем вторую стойку с тем же именем - logger.debug(f"Attempting to create second rack with name '{rack_name}'") - - # Нажимаем кнопку "Создать" на тулбаре - self.location_page.click_create_button() - - # Создаем фрейм создания дочернего элемента - create_child_frame = CreateChildElementFrame(browser) - - # Нажимаем на плашку "Класс объекта учета" - create_child_frame.open_object_class_combobox() - - # Из выпадающего меню выбираем пункт "Стойка" - create_child_frame.select_object_class("Стойка") - - # Открывается набор плашек для задания параметров стойки - rack_maker = RackObjectMaker(browser) - - # Создаем объект данных для второй стойки - rack_data = RackData( - name=rack_name, - height="42", - depth="450" - ) - - # Пытаемся создать вторую стойку с тем же именем - rack_maker.fill_rack_data(rack_data) - - # Нажимаем кнопку создания - create_child_frame.click_add_button() - create_child_frame.wait_for_timeout(1000) - - # Проверяем наличие alert-окна с сообщением о дублирующемся имени - alert = AlertComponent(browser) - expected_alert_text = f"Имя {rack_name} уже используется" - alert.check_alert_presence(expected_alert_text) - - # Закрываем alert-окно с помощью кнопки закрытия - create_child_frame.wait_for_timeout(2000) - alert.close_alert_by_text(expected_alert_text) - - # Проверяем, что остались на странице создания (стойка не создана) - create_child_frame.check_toolbar_title('Создать дочерний элемент в') - - logger.debug("System prevented creating rack with duplicate name") - - def test_required_fields_validation(self, browser: Page, cleanup_racks) -> None: - """Тест проверки обязательных полей при создании стойки. - - Проверяет, что система корректно валидирует обязательные поля: - - Поле 'Высота в юнитах' должно быть заполнено - - Поле 'Глубина (мм)' должно быть заполнено - """ - # Текст сообщения alert-окна - expected_alert_text_height = "поле Высота в юнитах должно быть заполнено" - expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено" - - # Нажимаем кнопку "Создать" на тулбаре - self.location_page.click_create_button() - - # Создаем фрейм создания дочернего элемента - create_child_frame = CreateChildElementFrame(browser) - - # Нажимаем на плашку "Класс объекта учета" - create_child_frame.open_object_class_combobox() - - # Из выпадающего меню выбираем пункт "Стойка" - create_child_frame.select_object_class("Стойка") - - # Открывается набор плашек для задания параметров стойки - rack_maker = RackObjectMaker(browser) - - # Тестовые данные - test_cases = [ - { - "name": "Test 1: Required fields are not filled", - "data": { - "name": "Test-Rack-Required-01", - "height": "", - "depth": "", - "expected_alert_height": expected_alert_text_height, - "expected_alert_depth": expected_alert_text_depth - } - }, - { - "name": "Test 2: Only 'Height in units' field is filled", - "data": { - "name": "Test-Rack-Required-02", - "height": "42", - "depth": "", - "expected_alert_height": expected_alert_text_height, - "expected_alert_depth": expected_alert_text_depth - } - }, - { - "name": "Test 3: Only 'Depth (mm)' field is filled", - "data": { - "name": "Test-Rack-Required-03", - "height": "", - "depth": "1000", - "expected_alert_height": expected_alert_text_height, - "expected_alert_depth": expected_alert_text_depth - } - } - ] - - # Выполняем тестовые случаи - for test_case in test_cases: - logger.debug(test_case["name"]) - self._perform_required_fields_test( - create_child_frame, rack_maker, test_case["data"]) - logger.debug("System prevented creating rack with invalid required fields") - - # 4. Тест: Заполняем все обязательные поля - logger.debug("Test 4: All required fields are filled") - - # Генерируем уникальное имя для финального теста - final_rack_name = "Test-Rack-Required-04" - cleanup_racks.append(final_rack_name) - - # **ВАЖНО: Очищаем поля перед заполнением** - logger.debug("Clearing fields before filling...") - - # Получаем контейнер формы - container_locator = create_child_frame.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1) - fields_locators = create_child_frame.get_input_fields_locators(container_locator) - - # Очищаем поле "Высота в юнитах" если оно заполнено - if "Высота в юнитах" in fields_locators: - if create_child_frame.is_field_filled("Высота в юнитах", container_locator): - logger.debug("Clearing 'Высота в юнитах' field...") - create_child_frame.clear_combobox_field("Высота в юнитах") - create_child_frame.wait_for_timeout(500) - - # Очищаем поле "Глубина (мм)" если оно заполнено - if "Глубина (мм)" in fields_locators: - if create_child_frame.is_field_filled("Глубина (мм)", container_locator): - logger.debug("Clearing 'Глубина (мм)' field...") - create_child_frame.clear_combobox_field("Глубина (мм)") - create_child_frame.wait_for_timeout(500) - - # Очищаем поле "Имя" если оно заполнено - if "Имя" in fields_locators: - if create_child_frame.is_field_filled("Имя", container_locator): - logger.debug("Clearing 'Имя' field...") - # Специальная обработка для текстового поля - field_container = fields_locators["Имя"] - input_field = field_container.locator("input").first - if input_field.count() > 0: - input_field.click() - input_field.press("Control+A") - input_field.press("Backspace") - create_child_frame.wait_for_timeout(500) - - # Создаем объект данных стойки - rack_data = RackData( - name=final_rack_name, - height="42", - depth="1000" - ) - - # Заполняем все обязательные поля - rack_maker.fill_rack_data(rack_data) - - # Проверяем, что ни одно поле не подсвечено цветом ошибки - create_child_frame.check_field_error_not_highlighted("Имя") - create_child_frame.check_field_error_not_highlighted("Высота в юнитах") - create_child_frame.check_field_error_not_highlighted("Глубина (мм)") - logger.debug("No required fields are highlighted with error color - all fields filled correctly") - - # Нажимаем кнопку создания - create_child_frame.click_add_button() - create_child_frame.wait_for_timeout(500) - - # Проверяем уведомление об успешном создании стойки - alert = AlertComponent(browser) - expected_alert_text = f"Элемент {final_rack_name} создан" - alert.check_alert_presence(expected_alert_text) - - # Закрываем alert - alert.close_alert_by_text(expected_alert_text) - - logger.debug("Required fields validation test completed successfully") - - # Вспомогательные методы проверки - - def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: - """Проверяет существование стойки. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для проверки - - Returns: - bool: True если стойка существует, False в противном случае - """ - logger.debug(f"Checking existence of rack with name '{rack_name}'") - - # Обновляем навигационную панель - self.main_page.click_main_navigation_panel_item("Объекты") - self.main_page.click_main_navigation_panel_item("Объекты") - self.main_page.click_subpanel_item("test-zone") - - nav_panel_locator = NavigationPanelLocators.TREEVIEW - - # Проверяем видимость элемента - element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first - - if element.is_visible(): - logger.debug(f"Rack with name '{rack_name}' found") - return True - - logger.debug(f"Rack with name '{rack_name}' not found") - return False diff --git a/tests/e2e/elements/test_create_rack.py b/tests/e2e/elements/test_create_rack.py new file mode 100644 index 0000000..31f742b --- /dev/null +++ b/tests/e2e/elements/test_create_rack.py @@ -0,0 +1,474 @@ +"""Тест создания дочернего элемента 'Стойка'.""" + +import pytest +from playwright.sync_api import Page +from tools.logger import get_logger +from locators.navigation_panel_locators import NavigationPanelLocators +from frames.create_element_frame import CreateElementFrame +from forms.create_rack_form import CreateRackForm, CreateRackData +from pages.location_page import LocationPage +from makers.edit_rack_maker import EditRackMaker +from pages.login_page import LoginPage +from pages.main_page import MainPage +from pages.rack_page import RackPage +from components.alert_component import AlertComponent + + +logger = get_logger("CREATE_RACK_TEST") +logger.setLevel("INFO") + + +class TestCreateRack: + """Тест создания дочернего элемента типа 'Стойка'.""" + + # Единое имя для тестовой стойки + TEST_RACK_NAME = "Test-Rack-Create" + + # Для теста с дубликатом используем отдельное имя + DUPLICATE_RACK_NAME = "Test-Rack-Duplicate" + + # Инициализируем атрибуты + main_page: MainPage = None + location_page: LocationPage = None + alert: AlertComponent = None + create_child_frame: CreateElementFrame = None + rack_form: CreateRackForm = None + + @pytest.fixture(scope="function", autouse=True) + def setup(self, browser: Page) -> None: + """Фикстура для подготовки тестового окружения. + + Args: + browser: Экземпляр страницы Playwright для взаимодействия с UI + """ + + # Авторизация в системе + login_page = LoginPage(browser) + login_page.do_login() + + # Мы на главной странице + self.main_page = MainPage(browser) + self.main_page.should_be_navigation_panel() + + # Переходим к Объектам + self.main_page.click_main_navigation_panel_item("Объекты") + self.main_page.wait_for_timeout(1000) + self.main_page.click_main_navigation_panel_item("test-zone") + + # Создаем экземпляр страницы локации + self.location_page = LocationPage(browser) + + # Инициализируем компонент алертов + self.alert = AlertComponent(browser) + + # Инициализируем фрейм создания дочернего элемента + self.create_child_frame = CreateElementFrame(browser) + + # Инициализируем форму создания Стойки + self.rack_form = CreateRackForm(browser) + + @pytest.fixture + def cleanup_racks(self, browser: Page): + """Фикстура для очистки созданных стоек.""" + + created_racks = [] + yield created_racks + + # После завершения теста удаляем созданные стойки + if created_racks: + logger.debug(f"Cleaning up racks: {created_racks}") + + self.main_page.wait_for_timeout(500) + self.main_page.click_subpanel_item("test-zone") + self.main_page.wait_for_timeout(1000) + + for rack_name in created_racks: + if self._check_rack_existance(browser, rack_name): + logger.debug(f"Deleting rack '{rack_name}'...") + self.main_page.click_subpanel_item(rack_name, parent="test-zone") + self.main_page.wait_for_timeout(1000) + self._delete_rack(browser, rack_name) + self.main_page.click_subpanel_item("test-zone") + self.main_page.wait_for_timeout(500) + + def _create_rack(self, browser: Page, rack_data: CreateRackData) -> None: + """Создает стойку с использованием унифицированного подхода. + + Args: + browser: Страница Playwright + rack_data: Данные стойки для создания + """ + + logger.debug(f"Creating rack with name '{rack_data.name}'") + + # Нажимаем кнопку "Создать" на тулбаре + self.location_page.click_create_button() + + # Нажимаем на плашку "Класс объекта учета" + self.create_child_frame.open_object_class_combobox() + + # Из выпадающего меню выбираем пункт "Стойка" + self.create_child_frame.select_object_class("Стойка") + + # Создаем форму создания стойки + rack_form = CreateRackForm(browser) + + # Заполняем данные стойки + fill_results = rack_form.fill_rack_data(rack_data) + logger.debug(f"Fill results: {fill_results}") + + # Нажимаем кнопку создания + self.create_child_frame.click_add_button() + + # Ждем появления alert с текстом + expected_alert_text = f"Элемент {rack_data.name} создан" + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + # Если уже закрылся - игнорируем + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.info(f"Rack '{rack_data.name}' created successfully") + + def _delete_rack(self, browser: Page, rack_name: str) -> None: + """Удаляет стойку через контекстное меню. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для удаления + """ + + # Находим элемент стойки в навигационной панели + rack_element = browser.locator( + NavigationPanelLocators.TREEVIEW + ).get_by_text(rack_name, exact=True).first + + # Прокручиваем до элемента если нужно + rack_element.scroll_into_view_if_needed() + self.main_page.wait_for_timeout(500) + + # Проверяем и нажимаем кнопку "Изменить" + rack_page = RackPage(browser) + + # Проверяем видимость и тултип кнопки + rack_page.should_be_toolbar_buttons() + + # Кликаем на кнопку "Изменить" + rack_page.click_edit_button() + self.main_page.wait_for_timeout(1000) + + # Создаем экземпляр EditRackMaker + rack_edit = EditRackMaker(browser, rack_name) + + # Используем метод для удаления + rack_edit.click_remove_button() + + # Проверяем уведомление об успешном удалении + expected_alert_text = "Успешно удалено" + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + # Если уже закрылся - игнорируем + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.info(f"Rack '{rack_name}' deleted successfully") + + def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: + """Проверяет существование стойки. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для проверки + + Returns: + bool: True если стойка существует, False в противном случае + """ + + logger.debug(f"Checking existence of rack with name '{rack_name}'") + + self.main_page.click_subpanel_item("test-zone") + + nav_panel_locator = NavigationPanelLocators.TREEVIEW + element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first + + if element.is_visible(): + logger.debug(f"Rack with name '{rack_name}' found") + return True + + logger.debug(f"Rack with name '{rack_name}' not found") + return False + + def test_create_rack_content(self, browser: Page) -> None: + """Тест проверки содержимого формы создания стойки.""" + + # Проверяем что кнопка "Создать" доступна + self.location_page.should_be_toolbar_buttons() + + # Нажимаем кнопку "Создать" на тулбаре + self.location_page.click_create_button() + + # Проверяем заголовок формы создания + self.create_child_frame.check_toolbar_title('Создать дочерний элемент в') + + # Нажимаем на плашку "Класс объекта учета" + self.create_child_frame.open_object_class_combobox() + + # Из выпадающего меню выбираем пункт "Стойка" + self.create_child_frame.select_object_class("Стойка") + + # Создаем форму создания стойки и проверяем наличие полей + rack_form = CreateRackForm(browser) + + # Проверяем, что основные поля присутствуют + assert rack_form.get_content_item("name_input") is not None, "Name field not initialized" + assert rack_form.get_content_item("usize_input") is not None, "Height field not initialized" + assert rack_form.get_content_item("depth_input") is not None, "Depth field not initialized" + + logger.debug("Rack-specific fields are displayed correctly") + self.create_child_frame.should_be_toolbar_buttons() + + def test_create_rack(self, browser: Page, cleanup_racks) -> None: + """Тест создания дочернего элемента типа 'Стойка'.""" + + logger.debug(f"Starting test with rack name: {self.TEST_RACK_NAME}") + + # Создаем данные стойки с расширенным набором полей + rack_data = CreateRackData( + name=self.TEST_RACK_NAME, + usize="42", + depth="1000", + serial="TEST123456", + inventory="INV-001", + comment="Тестовая стойка для автоматизации", + cable_entry="сверху", + state="Введен в эксплуатацию", + # owner="Владелец", + # service_org="Обслуживающая организация", + # project="Проект/Титул" + ) + + # Сохраняем имя стойки для очистки + cleanup_racks.append(rack_data.name) + + # Проверяем, существует ли уже стойка с таким именем + if self._check_rack_existance(browser, rack_data.name): + logger.warning(f"Rack '{rack_data.name}' already exists. Deleting it before creating new one...") + + # Переходим к стойке для удаления + self.main_page.click_subpanel_item(rack_data.name, parent="test-zone") + self.main_page.wait_for_timeout(1000) + + # Удаляем существующую стойку + self._delete_rack(browser, rack_data.name) + logger.debug(f"Existing rack '{rack_data.name}' deleted successfully") + + # Создаем новую стойку + self._create_rack(browser, rack_data) + + # Проверяем, что стойка создана и отображается + logger.debug(f"Verifying that rack '{rack_data.name}' was created...") + self.main_page.click_main_navigation_panel_item("test-zone") + + rack_exists = self._check_rack_existance(browser, rack_data.name) + assert rack_exists, f"Rack '{rack_data.name}' should be visible in navigation panel after creation" + + logger.debug(f"Rack '{rack_data.name}' is visible in navigation panel") + logger.debug("Test for creating 'Rack' child element completed successfully") + + def test_create_rack_with_duplicate_name(self, browser: Page, cleanup_racks) -> None: + """Тест создания стойки с уже существующим именем.""" + + logger.debug(f"Starting test for creating rack with duplicate name: {self.DUPLICATE_RACK_NAME}") + + rack_name = self.DUPLICATE_RACK_NAME + + # Создаем первую стойку если её нет + if not self._check_rack_existance(browser, rack_name): + logger.debug(f"Creating first rack with name '{rack_name}'") + + first_rack_data = CreateRackData( + name=rack_name, + usize="42", + depth="1000" + ) + self._create_rack(browser, first_rack_data) + cleanup_racks.append(rack_name) + + # Пытаемся создать вторую стойку с тем же именем + logger.debug(f"Attempting to create second rack with name '{rack_name}'") + + self.location_page.click_create_button() + + # Нажимаем на плашку "Класс объекта учета" + self.create_child_frame.open_object_class_combobox() + + # Из выпадающего меню выбираем пункт "Стойка" + self.create_child_frame.select_object_class("Стойка") + + rack_form = CreateRackForm(browser) + + duplicate_rack_data = CreateRackData( + name=rack_name, + usize="42", + depth="450" + ) + + self.create_child_frame.check_toolbar_title('Создать дочерний элемент в') + + rack_form.fill_rack_data(duplicate_rack_data) + self.create_child_frame.click_add_button() + + expected_alert_text = f"Имя {rack_name} уже используется" + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + # Если уже закрылся - игнорируем + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.debug("System prevented creating rack with duplicate name") + + def test_required_fields_validation(self, browser: Page, cleanup_racks) -> None: + """Тест проверки обязательных полей при создании стойки.""" + logger.debug("Starting required fields validation test") + + expected_alert_text_height = "поле Высота в юнитах должно быть заполнено" + expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено" + expected_alert_text_name = "Поле Имя должно быть установлено" + + self.main_page.click_main_navigation_panel_item("test-zone") + + # Открываем форму создания + self.location_page.click_create_button() + + # Нажимаем на плашку "Класс объекта учета" + self.create_child_frame.open_object_class_combobox() + + # Из выпадающего меню выбираем пункт "Стойка" + self.create_child_frame.select_object_class("Стойка") + + rack_form = CreateRackForm(browser) + + # ========== Тест 1: Обязательные поля высота и глубина пустые ========== + logger.debug("Test 1: Both required fields (height, depth) are empty") + + # Очищаем поля высоты и глубины перед заполнением + rack_form.clear_field("Высота в юнитах") + rack_form.clear_field("Глубина (мм)") + + test_data_1 = CreateRackData( + name=self.TEST_RACK_NAME, + usize="", + depth="" + ) + + rack_form.fill_rack_data(test_data_1) + self.create_child_frame.click_add_button() + self.create_child_frame.wait_for_timeout(500) + + # Проверяем alert для высоты, глубины + self.alert.check_alert_presence(expected_alert_text_height, timeout=5000) + self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000) + + # Проверяем, закрылся ли автоматически alert для высоты, глубины + self.alert.check_alert_absence(expected_alert_text_height, timeout=7000) + self.alert.check_alert_absence(expected_alert_text_depth, timeout=500) + + # Проверяем подсветку полей + field_status = rack_form.verify_required_fields_highlighted(["Высота в юнитах", "Глубина (мм)"]) + logger.debug(f"Field status after test 1: {field_status}") + assert field_status.get("Высота в юнитах"), f"Height field should be highlighted, got: {field_status}" + assert field_status.get("Глубина (мм)"), f"Depth field should be highlighted, got: {field_status}" + + # ========== Тест 2: Только высота заполнена ========== + logger.debug("Test 2: Only height field is filled") + + # Очищаем поле глубины перед новым заполнением + rack_form.clear_field("Глубина (мм)") + + test_data_2 = CreateRackData( + name=self.TEST_RACK_NAME, + usize="42", + depth="" + ) + + rack_form.fill_rack_data(test_data_2) + self.create_child_frame.click_add_button() + + # Проверяем alert для глубины + self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000) + + # Проверяем, закрылся ли автоматически alert для глубины + self.alert.check_alert_absence(expected_alert_text_depth, timeout=7000) + + # Проверяем подсветку полей + field_status = rack_form.verify_required_fields_highlighted(["Глубина (мм)"]) + logger.debug(f"Field status after test 2: {field_status}") + assert field_status.get("Глубина (мм)"), f"Depth field should be highlighted, got: {field_status}" + + # ========== Тест 3: Только глубина заполнена ========== + logger.debug("Test 3: Only depth field is filled") + + # Очищаем поле высоты перед новым заполнением + rack_form.clear_field("Высота в юнитах") + + test_data_3 = CreateRackData( + name=self.TEST_RACK_NAME, + usize="", + depth="1000" + ) + + rack_form.fill_rack_data(test_data_3) + self.create_child_frame.click_add_button() + + # Проверяем alert для высоты + self.alert.check_alert_presence(expected_alert_text_height, timeout=5000) + + # Проверяем, закрылся ли автоматически alert для высоты + self.alert.check_alert_absence(expected_alert_text_height, timeout=7000) + + # Проверяем подсветку полей + field_status = rack_form.verify_required_fields_highlighted(["Высота в юнитах"]) + logger.debug(f"Field status after test 3: {field_status}") + assert field_status.get("Высота в юнитах"), f"Height field should be highlighted, got: {field_status}" + + # ========== Тест 4: Поле "Имя" не заполнено ========== + logger.debug("Test 4: Name field is empty") + + # Очищаем поле имени + rack_form.clear_field("Имя") + + test_data_4 = CreateRackData( + name="", + usize="42", + depth="1000" + ) + + rack_form.fill_rack_data(test_data_4) + self.create_child_frame.click_add_button() + self.create_child_frame.wait_for_timeout(500) + + # Проверяем alert для имени + self.alert.check_alert_presence(expected_alert_text_name, timeout=5000) + + # Проверяем, закрылся ли автоматически alert для высоты + self.alert.check_alert_absence(expected_alert_text_name, timeout=7000) + + logger.debug("Test 4 completed: System correctly validates empty name field") diff --git a/tests/e2e/elements/test_edit_rack.py b/tests/e2e/elements/test_edit_rack.py index 8478a13..c910c9e 100644 --- a/tests/e2e/elements/test_edit_rack.py +++ b/tests/e2e/elements/test_edit_rack.py @@ -1,157 +1,45 @@ -"""Модуль тестов вкладки 'Стойка' в модуле Объекты. +"""Модуль тестов редактирования стойки в модуле Объекты. Содержит тесты для проверки функциональности -работы со стойкой оборудования. +редактирования стойки оборудования. """ import os import pytest from playwright.sync_api import Page +from tools.logger import get_logger from locators.navigation_panel_locators import NavigationPanelLocators +from frames.create_element_frame import CreateElementFrame +from forms.create_rack_form import CreateRackForm, CreateRackData +from makers.edit_rack_maker import EditRackMaker, EditRackData +from pages.location_page import LocationPage from pages.login_page import LoginPage from pages.main_page import MainPage -from pages.location_page import LocationPage from pages.rack_page import RackPage -from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData -from components_derived.frames.create_child_element_frame import CreateChildElementFrame -from components_derived.modal_edit_rack import ModalEditRack, RackEditData from components.alert_component import AlertComponent -from tools.logger import get_logger -# Константы -RACK_NAME = "Test-Rack-Functionality" -# Инициализация логгера для всего модуля logger = get_logger("RACK_EDIT_TESTS") logger.setLevel("INFO") -class TestRackTab: - """Набор тестов для вкладки 'Стойка' в модуле Объекты. - Проверяет корректность отображения, функциональность элементов интерфейса - и переключение между вкладками стойки оборудования. +class TestRackEdit: + """Набор тестов для редактирования стойки в модуле Объекты. - Тесты покрывают следующие функциональные области: - 1. test_rack_general_info_tab_fields - Заполнение полей вкладки 'Общая информация' - 2. test_rack_image_tab - Работа с вкладкой 'Изображение' - 3. test_rack_access_rules - Заполнение полей правил доступа + Проверяет функциональность редактирования различных вкладок стойки: + 1. Общая информация + 2. Изображение + 3. Правила доступа """ + # Имя тестовой стойки + RACK_NAME = "Test-Rack-Edit" + # Инициализируем атрибуты main_page: MainPage = None location_page: LocationPage = None - - def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: - """Проверяет существование стойки. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для проверки - - Returns: - bool: True если стойка существует, False в противном случае - """ - - # Обновляем навигационную панель - self.main_page.wait_for_timeout(500) - self.main_page.click_subpanel_item("test-zone") - - nav_panel_locator = NavigationPanelLocators.TREEVIEW - - # Проверяем видимость элемента - element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first - - if element.is_visible(): - return True - return False - - def _create_rack(self, browser: Page, rack_name: str) -> None: - """Создает стойку. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для создания - """ - logger.debug(f"Creating rack: {rack_name}") - - # Нажимаем кнопку "Создать" на тулбаре - self.location_page.click_create_button() - - # Создаем фрейм создания дочернего элемента - create_child_frame = CreateChildElementFrame(browser) - - # Нажимаем на плашку "Класс объекта учета" - create_child_frame.open_object_class_combobox() - - # Из выпадающего меню выбираем пункт "Стойка" - create_child_frame.select_object_class("Стойка") - - # Открывается набор плашек для задания параметров стойки - rack_maker = RackObjectMaker(browser) - - # Создаем объект данных стойки - rack_data = RackData( - name=rack_name, - height="42", - depth="1000" - ) - - # Заполняем данные стойки - rack_maker.fill_rack_data(rack_data) - - # Нажимаем кнопку создания - create_child_frame.click_add_button() - - # Проверяем уведомление об успешном создании - alert = AlertComponent(browser) - expected_alert_text = f"Элемент {rack_name} создан" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) - - logger.info(f"Rack '{rack_name}' created successfully") - - def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None: - """Удаляет стойку через контекстное меню. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для удаления - """ - - # 1. Находим элемент стойки в навигационной панели - rack_element = browser.locator( - NavigationPanelLocators.TREEVIEW - ).get_by_text(rack_name, exact=True).first - - # Прокручиваем до элемента если нужно - rack_element.scroll_into_view_if_needed() - self.main_page.wait_for_timeout(500) - - # 2. Проверяем и нажимаем кнопку "Изменить" - rack_page = RackPage(browser) - - # Проверяем видимость и тултип кнопки - rack_page.should_be_toolbar_buttons() - - # Кликаем на кнопку "Изменить" - rack_page.click_edit_button() - - self.main_page.wait_for_timeout(1000) - - # 3. Создаем экземпляр ModalRackEditRack - rack_edit = ModalEditRack(browser, rack_name) - - # Используем метод для удаления - rack_edit.click_remove_button() - self.main_page.wait_for_timeout(1000) - - # 4. Проверяем уведомление об успешном удалении - alert = AlertComponent(browser) - expected_alert_text = "Успешно удалено" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) - - logger.info(f"Rack '{rack_name}' deleted successfully") + alert: AlertComponent = None + create_child_frame: CreateElementFrame = None @pytest.fixture(scope="function", autouse=True) def setup(self, browser: Page) -> None: @@ -159,11 +47,13 @@ class TestRackTab: Выполняет: 1. Авторизацию в системе - 2. Создание стойки если она не существует - 3. Переход к стойке + 2. Переход к локации test-zone + 3. Инициализацию компонентов + 4. Создание стойки если она не существует + 5. Переход к стойке Args: - browser (Page): Экземпляр страницы Playwright для взаимодействия с UI + browser: Экземпляр страницы Playwright для взаимодействия с UI """ # Авторизация в системе @@ -182,22 +72,35 @@ class TestRackTab: # Создаем экземпляр страницы локации self.location_page = LocationPage(browser) + # Инициализируем компонент алертов (вынесено в атрибуты класса) + self.alert = AlertComponent(browser) + + # Инициализируем фрейм создания дочернего элемента (вынесено в атрибуты класса) + self.create_child_frame = CreateElementFrame(browser) + # Проверяем существование стойки - if not self._check_rack_existance(browser, RACK_NAME): - self._create_rack(browser, RACK_NAME) + if not self._check_rack_existance(browser, self.RACK_NAME): + logger.info(f"Rack '{self.RACK_NAME}' does not exist. Creating...") + + rack_data = CreateRackData( + name=self.RACK_NAME, + usize="42", + depth="1000" + ) + self._create_rack(browser, rack_data) self.main_page.wait_for_timeout(3000) else: - logger.info(f"Rack '{RACK_NAME}' already exists") + logger.info(f"Rack '{self.RACK_NAME}' already exists") # Переходим к стойке для тестирования - self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") + self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone") self.main_page.wait_for_timeout(3000) @pytest.fixture(scope="class", autouse=True) def cleanup_rack(self, browser: Page): """Фикстура для очистки созданной стойки после ВСЕХ тестов класса. - Выполняется один раз после завершения всех тестов класса TestRackTab. + Выполняется один раз после завершения всех тестов класса TestRackEdit. Удаляет созданную стойку. Args: @@ -207,6 +110,8 @@ class TestRackTab: # Тесты выполняются здесь yield + logger.debug(f"Cleaning up rack: {self.RACK_NAME}") + # Переходим на главную страницу и в нужную зону login_page = LoginPage(browser) login_page.do_login() @@ -220,37 +125,160 @@ class TestRackTab: self.main_page.click_main_navigation_panel_item("test-zone") self.main_page.wait_for_timeout(1000) + # Инициализируем компонент алертов для cleanup + self.alert = AlertComponent(browser) + # Проверяем существование стойки - if self._check_rack_existance(browser, RACK_NAME): + if self._check_rack_existance(browser, self.RACK_NAME): # Переходим на страницу стойки - self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") + self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone") self.main_page.wait_for_timeout(2000) # Удаляем стойку - self._delete_rack_from_context_menu(browser, RACK_NAME) + self._delete_rack(browser, self.RACK_NAME) # Дополнительная проверка self.main_page.click_subpanel_item("test-zone") self.main_page.wait_for_timeout(1000) - #@pytest.mark.develop + def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: + """Проверяет существование стойки. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для проверки + + Returns: + bool: True если стойка существует, False в противном случае + """ + + logger.debug(f"Checking existence of rack with name '{rack_name}'") + + self.main_page.click_subpanel_item("test-zone") + + nav_panel_locator = NavigationPanelLocators.TREEVIEW + element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first + + if element.is_visible(): + logger.debug(f"Rack with name '{rack_name}' found") + return True + + logger.debug(f"Rack with name '{rack_name}' not found") + return False + + def _create_rack(self, browser: Page, rack_data: CreateRackData) -> None: + """Создает стойку. + + Args: + browser: Страница Playwright + rack_data: Данные стойки для создания + """ + + logger.debug(f"Creating rack with name '{rack_data.name}'") + + # Нажимаем кнопку "Создать" на тулбаре + self.location_page.click_create_button() + + # Нажимаем на плашку "Класс объекта учета" + self.create_child_frame.open_object_class_combobox() + + # Из выпадающего меню выбираем пункт "Стойка" + self.create_child_frame.select_object_class("Стойка") + + # Создаем форму создания стойки + rack_form = CreateRackForm(browser) + + # Заполняем данные стойки + fill_results = rack_form.fill_rack_data(rack_data) + logger.debug(f"Fill results: {fill_results}") + + # Нажимаем кнопку создания + self.create_child_frame.click_add_button() + + # Ждем появления alert с текстом + expected_alert_text = f"Элемент {rack_data.name} создан" + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + # Если уже закрылся - игнорируем + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.info(f"Rack '{rack_data.name}' created successfully") + + def _delete_rack(self, browser: Page, rack_name: str) -> None: + """Удаляет стойку через контекстное меню. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для удаления + """ + + # Находим элемент стойки в навигационной панели + rack_element = browser.locator( + NavigationPanelLocators.TREEVIEW + ).get_by_text(rack_name, exact=True).first + + # Прокручиваем до элемента если нужно + rack_element.scroll_into_view_if_needed() + self.main_page.wait_for_timeout(500) + + # Проверяем и нажимаем кнопку "Изменить" + rack_page = RackPage(browser) + + # Проверяем видимость и тултип кнопки + rack_page.should_be_toolbar_buttons() + + # Кликаем на кнопку "Изменить" + rack_page.click_edit_button() + self.main_page.wait_for_timeout(1000) + + # Создаем экземпляр EditRackMaker + rack_edit = EditRackMaker(browser, rack_name) + + # Используем метод для удаления + rack_edit.click_remove_button() + + # Проверяем уведомление об успешном удалении + expected_alert_text = "Успешно удалено" + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + # Если уже закрылся - игнорируем + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.info(f"Rack '{rack_name}' deleted successfully") + def test_rack_general_info_tab_fields(self, browser: Page) -> None: """Тест заполнения полей вкладки 'Общая информация' стойки.""" + logger.debug(f"Starting general info tab test for rack: {self.RACK_NAME}") + rack_page = RackPage(browser) # Переходим в режим редактирования rack_page.click_edit_button() rack_page.wait_for_timeout(1000) - # Создаем экземпляр ModalEditRack - rack_edit = ModalEditRack(browser, RACK_NAME) + # Создаем экземпляр EditRackMaker + rack_edit = EditRackMaker(browser, self.RACK_NAME) # Создаем тестовые данные для заполнения всех полей - rack_edit_data = RackEditData( + rack_edit_data = EditRackData( # Основные поля - name=RACK_NAME, + name=self.RACK_NAME, serial="SN123456789", inventory="INV987654321", comment="Тестовый комментарий для стойки (обновленный)", @@ -274,15 +302,19 @@ class TestRackTab: # Сохраняем изменения rack_edit.click_done_button() - rack_edit.wait_for_timeout(2000) # Проверяем уведомление об успешном обновлении - alert = AlertComponent(browser) expected_alert_text = "Элемент успешно обновлён" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) + self.alert.check_alert_presence(expected_alert_text, timeout=5000) - browser.mouse.click(10, 10) + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + logger.debug("Alert already closed by the time forcible close was attempted") # Вход в режим редактирования rack_page.click_edit_button() @@ -302,24 +334,27 @@ class TestRackTab: rack_edit.click_close_button() - #@pytest.mark.develop + logger.debug("General info tab test completed successfully") + def test_rack_image_tab(self, browser: Page) -> None: """Тест вкладки 'Изображение' стойки.""" + logger.debug(f"Starting image tab test for rack: {self.RACK_NAME}") + rack_page = RackPage(browser) # Переходим в режим редактирования rack_page.click_edit_button() rack_page.wait_for_timeout(1000) - # Создаем экземпляр ModalEditRack - rack_edit = ModalEditRack(browser, RACK_NAME) + # Создаем экземпляр EditRackMaker + rack_edit = EditRackMaker(browser, self.RACK_NAME) # Переключаемся на вкладку "Изображение" - rack_edit.switch_to_tab(ModalEditRack.TAB_IMAGE) + rack_edit.switch_to_tab(EditRackMaker.TAB_IMAGE) # Проверяем вкладку - assert rack_edit.is_tab_active(ModalEditRack.TAB_IMAGE), "Image tab should be active" + assert rack_edit.is_tab_active(EditRackMaker.TAB_IMAGE), "Image tab should be active" # Загружаем изображение если есть test_image_path = os.path.join(os.path.dirname(__file__), "test_edit_rack_image.jpg") @@ -339,35 +374,44 @@ class TestRackTab: # Сохраняем rack_edit.click_done_button() - rack_page.wait_for_timeout(2000) # Проверяем уведомление об успешном обновлении - alert = AlertComponent(browser) expected_alert_text = "Элемент успешно обновлён" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.debug("Image tab test completed successfully") - @pytest.mark.develop def test_rack_access_rules(self, browser: Page) -> None: """Тест заполнения полей правил доступа. В каждое поле добавляются ВСЕ пользователи из списка custom_users. """ + logger.debug(f"Starting access rules test for rack: {self.RACK_NAME}") + rack_page = RackPage(browser) # Переходим в режим редактирования rack_page.click_edit_button() rack_page.wait_for_timeout(1000) - # Создаем экземпляр ModalEditRack - rack_edit = ModalEditRack(browser, RACK_NAME) + # Создаем экземпляр EditRackMaker + rack_edit = EditRackMaker(browser, self.RACK_NAME) # Переключаемся на вкладку "Настройки" - rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS) + rack_edit.switch_to_tab(EditRackMaker.TAB_SETTINGS) # Проверяем, что вкладка активна - assert rack_edit.is_tab_active(ModalEditRack.TAB_SETTINGS), \ + assert rack_edit.is_tab_active(EditRackMaker.TAB_SETTINGS), \ "Settings tab should be active after switching" # Целевые поля для заполнения @@ -420,20 +464,26 @@ class TestRackTab: # Сохраняем изменения rack_edit.click_done_button() - rack_page.wait_for_timeout(2000) # Проверяем уведомление об успешном обновлении - alert = AlertComponent(browser) expected_alert_text = "Элемент успешно обновлён" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + logger.debug("Alert already closed by the time forcible close was attempted") # Возвращаемся в режим редактирования и проверяем снова rack_page.click_edit_button() rack_page.wait_for_timeout(1000) - rack_edit = ModalEditRack(browser, RACK_NAME) - rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS) + rack_edit = EditRackMaker(browser, self.RACK_NAME) + rack_edit.switch_to_tab(EditRackMaker.TAB_SETTINGS) verification_results_after_save = rack_edit.verify_access_rules( expected_users=custom_users, @@ -445,3 +495,5 @@ class TestRackTab: f"After save - correctly filled {verification_results_after_save['correctly_filled']} out of {expected_total}" rack_edit.click_close_button() + + logger.debug("Access rules test completed successfully") diff --git a/tests/e2e/elements/test_management_rack.py b/tests/e2e/elements/test_management_rack.py index 20c6f66..a7685d6 100644 --- a/tests/e2e/elements/test_management_rack.py +++ b/tests/e2e/elements/test_management_rack.py @@ -6,149 +6,37 @@ import pytest from playwright.sync_api import Page +from tools.logger import get_logger from locators.navigation_panel_locators import NavigationPanelLocators -from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData -from components_derived.frames.create_child_element_frame import CreateChildElementFrame -from components_derived.modal_edit_rack import ModalEditRack +from frames.create_element_frame import CreateElementFrame +from forms.create_rack_form import CreateRackForm, CreateRackData +from makers.edit_rack_maker import EditRackMaker from pages.location_page import LocationPage from pages.login_page import LoginPage from pages.main_page import MainPage from pages.rack_page import RackPage from components.alert_component import AlertComponent -from tools.logger import get_logger -# Константы -RACK_NAME = "Test-Rack-Functionality" -# Инициализация логгера для всего модуля logger = get_logger("RACK_MANAGEMENT_TESTS") logger.setLevel("INFO") -class TestRackTab: + +class TestRackManagement: """Набор тестов для вкладки 'Стойка' в модуле Объекты. Проверяет корректность отображения, функциональность элементов интерфейса и переключение между вкладками стойки оборудования. - - Тесты покрывают следующие функциональные области: - 1. test_rack_tab_content - Базовая структура и содержимое вкладки стойки """ + # Имя тестовой стойки + RACK_NAME = "Test-Rack-Functionality" + # Инициализируем атрибуты main_page: MainPage = None location_page: LocationPage = None - - def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: - """Проверяет существование стойки. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для проверки - - Returns: - bool: True если стойка существует, False в противном случае - """ - - # Обновляем навигационную панель - self.main_page.wait_for_timeout(500) - self.main_page.click_subpanel_item("test-zone") - - nav_panel_locator = NavigationPanelLocators.TREEVIEW - - # Проверяем видимость элемента - element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first - - if element.is_visible(): - return True - return False - - def _create_rack(self, browser: Page, rack_name: str) -> None: - """Создает стойку. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для создания - """ - logger.debug(f"Creating rack: {rack_name}") - - # Нажимаем кнопку "Создать" на тулбаре - self.location_page.click_create_button() - - # Создаем фрейм создания дочернего элемента - create_child_frame = CreateChildElementFrame(browser) - - # Нажимаем на плашку "Класс объекта учета" - create_child_frame.open_object_class_combobox() - - # Из выпадающего меню выбираем пункт "Стойка" - create_child_frame.select_object_class("Стойка") - - # Открывается набор плашек для задания параметров стойки - rack_maker = RackObjectMaker(browser) - - # Создаем объект данных стойки - rack_data = RackData( - name=rack_name, - height="42", - depth="1000" - ) - - # Заполняем данные стойки - rack_maker.fill_rack_data(rack_data) - - # Нажимаем кнопку создания - create_child_frame.click_add_button() - - # Проверяем уведомление об успешном создании - alert = AlertComponent(browser) - expected_alert_text = f"Элемент {rack_name} создан" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) - - logger.info(f"Rack '{rack_name}' created successfully") - - def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None: - """Удаляет стойку через контекстное меню. - - Args: - browser: Страница Playwright - rack_name: Имя стойки для удаления - """ - - # 1. Находим элемент стойки в навигационной панели - rack_element = browser.locator( - NavigationPanelLocators.TREEVIEW - ).get_by_text(rack_name, exact=True).first - - # Прокручиваем до элемента если нужно - rack_element.scroll_into_view_if_needed() - self.main_page.wait_for_timeout(500) - - # 2. Проверяем и нажимаем кнопку "Изменить" - rack_page = RackPage(browser) - - # Проверяем видимость и тултип кнопки - rack_page.should_be_toolbar_buttons() - - # Кликаем на кнопку "Изменить" - rack_page.click_edit_button() - - self.main_page.wait_for_timeout(1000) - - # 3. Создаем экземпляр ModalRackEditRack - rack_edit = ModalEditRack(browser, rack_name) - - # Используем метод для удаления - rack_edit.click_remove_button() - self.main_page.wait_for_timeout(1000) - - # 4. Проверяем уведомление об успешном удалении - alert = AlertComponent(browser) - expected_alert_text = "Успешно удалено" - alert.check_alert_presence(expected_alert_text) - alert.close_alert_by_text(expected_alert_text) - - logger.info(f"Rack '{rack_name}' deleted successfully") + alert: AlertComponent = None + create_child_frame: CreateElementFrame = None @pytest.fixture(scope="function", autouse=True) def setup(self, browser: Page) -> None: @@ -156,12 +44,15 @@ class TestRackTab: Выполняет: 1. Авторизацию в системе - 2. Создание стойки если она не существует - 3. Переход к стойке + 2. Переход к локации test-zone + 3. Инициализацию компонентов + 4. Создание стойки если она не существует + 5. Переход к стойке Args: - browser (Page): Экземпляр страницы Playwright для взаимодействия с UI + browser: Экземпляр страницы Playwright для взаимодействия с UI """ + # Авторизация в системе login_page = LoginPage(browser) login_page.do_login() @@ -178,15 +69,28 @@ class TestRackTab: # Создаем экземпляр страницы локации self.location_page = LocationPage(browser) + # Инициализируем компонент алертов (вынесено в атрибуты класса) + self.alert = AlertComponent(browser) + + # Инициализируем фрейм создания дочернего элемента (вынесено в атрибуты класса) + self.create_child_frame = CreateElementFrame(browser) + # Проверяем существование стойки - if not self._check_rack_existance(browser, RACK_NAME): - self._create_rack(browser, RACK_NAME) + if not self._check_rack_existance(browser, self.RACK_NAME): + logger.info(f"Rack '{self.RACK_NAME}' does not exist. Creating...") + + rack_data = CreateRackData( + name=self.RACK_NAME, + usize="42", + depth="1000" + ) + self._create_rack(browser, rack_data) self.main_page.wait_for_timeout(3000) else: - logger.info(f"Rack '{RACK_NAME}' already exists") + logger.info(f"Rack '{self.RACK_NAME}' already exists") # Переходим к стойке для тестирования - self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") + self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone") self.main_page.wait_for_timeout(3000) @pytest.fixture(scope="class", autouse=True) @@ -199,9 +103,12 @@ class TestRackTab: Args: browser: Экземпляр страницы Playwright """ + # Тесты выполняются здесь yield + logger.debug(f"Cleaning up rack: {self.RACK_NAME}") + # Переходим на главную страницу и в нужную зону login_page = LoginPage(browser) login_page.do_login() @@ -215,21 +122,142 @@ class TestRackTab: self.main_page.click_main_navigation_panel_item("test-zone") self.main_page.wait_for_timeout(1000) + # Инициализируем компонент алертов для cleanup + self.alert = AlertComponent(browser) + # Проверяем существование стойки - if self._check_rack_existance(browser, RACK_NAME): + if self._check_rack_existance(browser, self.RACK_NAME): # Переходим на страницу стойки - self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") + self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone") self.main_page.wait_for_timeout(2000) # Удаляем стойку - self._delete_rack_from_context_menu(browser, RACK_NAME) + self._delete_rack(browser, self.RACK_NAME) # Дополнительная проверка self.main_page.click_subpanel_item("test-zone") self.main_page.wait_for_timeout(1000) - #@pytest.mark.develop + def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: + """Проверяет существование стойки. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для проверки + + Returns: + bool: True если стойка существует, False в противном случае + """ + + logger.debug(f"Checking existence of rack with name '{rack_name}'") + + self.main_page.click_subpanel_item("test-zone") + + nav_panel_locator = NavigationPanelLocators.TREEVIEW + element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first + + if element.is_visible(): + logger.debug(f"Rack with name '{rack_name}' found") + return True + + logger.debug(f"Rack with name '{rack_name}' not found") + return False + + def _create_rack(self, browser: Page, rack_data: CreateRackData) -> None: + """Создает стойку. + + Args: + browser: Страница Playwright + rack_data: Данные стойки для создания + """ + + logger.debug(f"Creating rack with name '{rack_data.name}'") + + # Нажимаем кнопку "Создать" на тулбаре + self.location_page.click_create_button() + + # Нажимаем на плашку "Класс объекта учета" + self.create_child_frame.open_object_class_combobox() + + # Из выпадающего меню выбираем пункт "Стойка" + self.create_child_frame.select_object_class("Стойка") + + # Создаем форму создания стойки + rack_form = CreateRackForm(browser) + + # Заполняем данные стойки + fill_results = rack_form.fill_rack_data(rack_data) + logger.debug(f"Fill results: {fill_results}") + + # Нажимаем кнопку создания + self.create_child_frame.click_add_button() + + # Ждем появления alert с текстом + expected_alert_text = f"Элемент {rack_data.name} создан" + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + # Если уже закрылся - игнорируем + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.info(f"Rack '{rack_data.name}' created successfully") + + def _delete_rack(self, browser: Page, rack_name: str) -> None: + """Удаляет стойку через контекстное меню. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для удаления + """ + + # Находим элемент стойки в навигационной панели + rack_element = browser.locator( + NavigationPanelLocators.TREEVIEW + ).get_by_text(rack_name, exact=True).first + + # Прокручиваем до элемента если нужно + rack_element.scroll_into_view_if_needed() + self.main_page.wait_for_timeout(500) + + # Проверяем и нажимаем кнопку "Изменить" + rack_page = RackPage(browser) + + # Проверяем видимость и тултип кнопки + rack_page.should_be_toolbar_buttons() + + # Кликаем на кнопку "Изменить" + rack_page.click_edit_button() + self.main_page.wait_for_timeout(1000) + + # Создаем экземпляр EditRackMaker + rack_edit = EditRackMaker(browser, rack_name) + + # Используем метод для удаления + rack_edit.click_remove_button() + + # Проверяем уведомление об успешном удалении + expected_alert_text = "Успешно удалено" + self.alert.check_alert_presence(expected_alert_text, timeout=5000) + + self.alert.check_alert_absence(expected_alert_text, timeout=7000) + + # Закрываем alert с текстом кнопкой 'Закрыть' + try: + self.alert.close_alert_by_text(expected_alert_text) + logger.debug("Alert forcibly closed") + except AssertionError: + # Если уже закрылся - игнорируем + logger.debug("Alert already closed by the time forcible close was attempted") + + logger.info(f"Rack '{rack_name}' deleted successfully") + def test_rack_tab_content(self, browser: Page) -> None: """Тест содержимого вкладки 'Стойка'. @@ -240,30 +268,34 @@ class TestRackTab: 4. Корректность отображения юнитов и устройств на стойке Args: - browser (Page): Экземпляр страницы Playwright для взаимодействия с UI + browser: Экземпляр страницы Playwright для взаимодействия с UI """ + logger.debug(f"Starting test for rack tab content with rack: {self.RACK_NAME}") + expected_toolbar_subtitles = [ "test-zone", 'chevron_right', - RACK_NAME - ] + self.RACK_NAME + ] - rt = RackPage(browser) - rt.should_be_panel_header(expected_toolbar_subtitles) + rack_page = RackPage(browser) + rack_page.should_be_panel_header(expected_toolbar_subtitles) # Комплексная проверка отображения обеих сторон стойки с детальной информацией - rt.should_be_rack_sides_displayed() + rack_page.should_be_rack_sides_displayed() # Проверка кнопки "Скрыть стойку" - rt.should_have_hide_rack_button() + rack_page.should_have_hide_rack_button() # Проверка кнопки "Показать стойку" - rt.should_have_show_rack_button() + rack_page.should_have_show_rack_button() # Проверяем переключение между всеми вкладками стойки - rt.check_tab_switching() + rack_page.check_tab_switching() - # Переход в режим редактирования - rt.should_be_toolbar_buttons() - rt.wait_for_timeout(1000) + # Проверям наличие кнопки редактирования + rack_page.should_be_toolbar_buttons() + rack_page.wait_for_timeout(1000) + + logger.debug("Test for rack tab content completed successfully")