"""Модуль компонента таблицы. Содержит класс для работы с табличными данными.""" from datetime import datetime from playwright.sync_api import Page, expect, Locator from tools.logger import get_logger from components.base_component import BaseComponent logger = get_logger("TABLE") class TableComponent(BaseComponent): """Компонент таблицы. Предоставляет методы для работы с табличными данными.""" def __init__(self, page: Page): """Инициализирует компонент таблицы. Args: page: Экземпляр страницы Playwright. """ super().__init__(page) # Действия: def click_arrow_button(self, table_locator: str | Locator, index: int) -> None: """ Нажатие кнопки-стрелочки вверх/вниз в ячейке заголовка таблицы Args: table_locator: Локатор таблицы. index: Индекс ячейки в заголовке. """ arrow_button = self.get_header_cell_button(table_locator, index) assert arrow_button.is_enabled(), f"Arrow button is missing in {index} header cell" arrow_button.click() def datetime2timestamp(self, date_string: str) -> float|None: """ Конвертация строкового представления даты и времени в Unix timestamp Args: date_string: Строка с датой и временем в формате d.m.Y H:M:S. Returns: float: Unix timestamp. None: конвертация невозможна """ # Формат, соответствующий строке с датой и временем format_string = "%d.%m.%Y %H:%M:%S" try: date_object = datetime.strptime(date_string, format_string) return date_object.timestamp() except ValueError: return None def get_arrow_button_state(self, table_locator: str | Locator, index: int) -> str: """ Получение состояния кнопки-стрелочки вверх/вниз в ячейке заголовка таблицы Args: table_locator: Локатор таблицы. index: Индекс ячейки в заголовке. Returns: up, если это стрелочка вверх. down, если это стрелочка вниз. """ arrow_button = self.get_header_cell_button(table_locator, index) assert arrow_button.is_enabled(), f"Arrow button is missing in {index} header cell" state = arrow_button.inner_text() if state == "keyboard_arrow_up": return "up" elif state == "keyboard_arrow_down": return "down" else: assert False, f"Got unsupported arrow state: {state}" def get_header_cell_button(self, table_locator: str | Locator, index: int) -> Locator: """ Поиск кнопки в ячейке заголовка таблицы Args: table_locator: Локатор таблицы. index: Индекс ячейки в заголовке. Returns: Локатор строки кнопки. Raises: AssertionError: Если индекс вне диапазона. """ table = self.get_locator(table_locator) header_cells_count = table.locator("//thead/tr/th").count() assert index in range(header_cells_count), "Header cell index is out of range" return table.locator("//thead/tr/th").nth(index).get_by_role("button") def get_row_locator(self, table_locator: str | Locator, row_index: int) -> Locator | None: """Возвращает локатор строки по индексу. Args: table_locator: Локатор таблицы. row_index: Индекс строки. Returns: Локатор строки или None, если индекс вне диапазона. """ table = self.get_locator(table_locator) rows = table.locator("//tbody/tr") if row_index in range(rows.count()): return rows.nth(row_index) else: return None def get_rows_count(self, locator: str | Locator) -> int: """Возвращает количество строк в таблице (без заголовка). Returns: int: Количество строк с данными. Raises: AssertionError: Если таблица пуста. """ table_content = self.read(locator) rows_count = len(table_content) if rows_count == 0: assert False, "The contents of the table are missing" return rows_count - 1 def read(self, locator: str | Locator) -> list[list[str]]: """Читает данные таблицы, включая заголовки. Args: locator: Локатор таблицы. Returns: Двумерный список с данными таблицы. """ table_data = [] table = self.get_locator(locator) # Чтение заголовка таблицы header_cells = table.locator("//thead/tr") header_cell_text = header_cells.nth(0).inner_text() header_data = header_cell_text.split('\n') table_data.append(header_data) # Чтение ячеек таблицы rows = table.locator("//tbody/tr") for i in range(rows.count()): row = rows.nth(i) cells = row.locator("td") row_data = [] for j in range(cells.count()): cell_text = cells.nth(j).inner_text() row_data.append(cell_text) table_data.append(row_data) return table_data # Проверки: def check_table_headers(self, actual_headers, expected_headers) -> None: """ Проверка соответствия заголовка таблицы ожидаемому""" is_equals = True arrow_state = ["keyboard_arrow_down", "keyboard_arrow_up"] for item in actual_headers: item = item.strip() if item in arrow_state: continue if item not in expected_headers: is_equals = False assert is_equals, \ f"Expected events table headers {expected_headers} are not equal {actual_headers}" def check_content(self, locator: str | Locator, expected_headers: list[str], check_table_not_empty: bool = True) -> None: """Проверяет содержимое таблицы. Проверяет заголовки и наличие данных в таблице. Args: locator: Локатор таблицы. expected_headers: Список ожидаемых заголовков таблицы. check_table_not_empty: Флаг проверки, что таблица не пустая. По умолчанию True. Raises: AssertionError: Если таблица пуста (при check_table_not_empty=True) или заголовки неверны. """ table_content = self.read(locator) if len(table_content) == 0: assert False, "The contents of the table are missing" # Проверка заголовков таблицы self.check_table_headers(table_content[0], expected_headers) # Проверка наличия данных в таблице if len(table_content) == 1: if check_table_not_empty: assert False, "Table body is missing" else: logger.info("Таблица пустая (не содержит строк с данными)") def check_column_descending_order(self, locator: str | Locator, index: int, convert2timestamp=False) -> bool: """Проверка, что заданный столбец таблицы упорядочен по убыванию. Args: locator: Локатор таблицы. index: Индекс столбца. convert2timestamp: Конвертировать строковое представление даты и времени в Unix timestamp Returns: True, если столбец таблицы упорядочен по убыванию. Иначе: False """ table_content = self.read(locator) if len(table_content) == 0: assert False, "The contents of the table are missing" del table_content[0] assert index in range(len(table_content[0])), \ "Column index is out of range" column = [] for i in range(len(table_content)): if convert2timestamp: timestamp = self.datetime2timestamp(table_content[i][index]) assert timestamp, f"Error conversation to timestamp for {table_content[i][index]}" column.append(timestamp) else: column.append(table_content[i][index]) return all(column[i] >= column[i+1] for i in range(len(column) - 1)) def check_first_row_visibility(self, locator: str | Locator) -> None: """Проверяет видимость первой строки таблицы. Args: locator: Локатор таблицы. """ table = self.get_locator(locator) first_row = table.locator("//tbody/tr").first expect(first_row).to_be_visible(), "The first table row is not visible" def check_last_row_visibility(self, locator: str | Locator) -> None: """Проверяет видимость последней строки таблицы. Args: locator: Локатор таблицы. """ table = self.get_locator(locator) last_row = table.locator("//tbody/tr").last expect(last_row).to_be_visible(), "The last table row is not visible" def check_row_highlighting(self, locator: str | Locator, row_index: int) -> None: """Проверяет изменение цвета строки при наведении. Args: locator: Локатор таблицы. row_index: Индекс проверяемой строки. """ table = self.get_locator(locator) row = table.locator("//tbody/tr").nth(row_index) row.scroll_into_view_if_needed() hover_element = row.locator(".body-row-hover") initial_color = hover_element.evaluate("el => window.getComputedStyle(el).backgroundColor") row.hover() self.page.wait_for_timeout(300) new_color = hover_element.evaluate("el => window.getComputedStyle(el).backgroundColor") assert initial_color != new_color, "Color of row did not change when hovering the cursor"