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/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/components_derived/frames/create_child_element_frame.py b/frames/create_element_frame.py similarity index 79% rename from components_derived/frames/create_child_element_frame.py rename to frames/create_element_frame.py index 7a5fff3..733bbb0 100644 --- a/components_derived/frames/create_child_element_frame.py +++ b/frames/create_element_frame.py @@ -11,11 +11,11 @@ from components.toolbar_component import ToolbarComponent from components_derived.selection_bar_component import SelectionBarComponent -logger = get_logger("CREATE_CHILD_ELEMENT_FRAME") - +logger = get_logger("CREATE_ELEMENT_FRAME") logger.setLevel("INFO") -class CreateChildElementFrame(BaseComponent): + +class CreateElementFrame(BaseComponent): """Фрейм создания дочернего элемента.""" def __init__(self, page: Page) -> 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/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")