384 lines
15 KiB
Python
384 lines
15 KiB
Python
"""Модуль компонента таблицы. Содержит класс для работы с табличными данными."""
|
||
|
||
import math
|
||
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, format_string = None) -> float|None:
|
||
""" Конвертация строкового представления даты и времени в Unix timestamp
|
||
Args:
|
||
date_string: Строка с датой и временем в формате d.m.Y H:M:S (default value).
|
||
|
||
Returns:
|
||
float: Unix timestamp.
|
||
None: конвертация невозможна
|
||
"""
|
||
|
||
# Формат, соответствующий строке с датой и временем
|
||
if format_string is 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_column(self, table_locator: str | Locator, index: int) -> list[str]:
|
||
"""Возвращает столбец таблицы по индексу.
|
||
|
||
Args:
|
||
table_locator: Локатор таблицы.
|
||
index: Индекс столбца.
|
||
|
||
Returns:
|
||
Список значений требуемого столбца.
|
||
"""
|
||
|
||
table_content = self.read(table_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)):
|
||
column.append(table_content[i][index])
|
||
return column
|
||
|
||
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 == '':
|
||
continue
|
||
|
||
if item not in expected_headers:
|
||
is_equals = False
|
||
|
||
assert is_equals, \
|
||
f"Expected 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"
|
||
|
||
def check_mui_table_row_highlighting(self, locator: str | Locator,
|
||
row_index: int,
|
||
offset_x: float,
|
||
offset_y: float,
|
||
scale_x: float,
|
||
scale_y: float) -> None:
|
||
"""Проверяет изменение цвета строки при наведении.
|
||
|
||
Args:
|
||
locator: Локатор таблицы.
|
||
row_index: Индекс проверяемой строки.
|
||
offset_x, offset_y: смещение координат таблицы относительно начала координат
|
||
scale_x, scale_y: коээфициенты масштабирования (причина: несовпадение масштабов контента страницы и фрейма)
|
||
"""
|
||
|
||
print("row_index: "+str(row_index))
|
||
table = self.get_locator(locator)
|
||
row = table.locator("tbody").locator(".MuiTableRow-root").nth(row_index)
|
||
# print(row)
|
||
|
||
# Прокручиваем и ждем
|
||
# row.scroll_into_view_if_needed()
|
||
# self.page.wait_for_timeout(5000)
|
||
|
||
# Получение "ограничительной рамки" строки
|
||
bounding_box = row.evaluate("el => el.getBoundingClientRect()")
|
||
print(bounding_box)
|
||
assert bounding_box, "Requested row is not visible"
|
||
|
||
# Получение текущего цвета фона
|
||
initial_color = row.evaluate("el => window.getComputedStyle(el).backgroundColor")
|
||
|
||
# Вычисление координат целевой строки таблицы и перевод на нее курсора мыши
|
||
bounding_box = row.evaluate("el => el.getBoundingClientRect()")
|
||
|
||
# center_x = (bounding_box["x"] + bounding_box["width"] / 2 + offset_x) * scale_x
|
||
# center_y = (bounding_box["y"] + bounding_box["height"] / 2 + offset_y) * scale_y
|
||
|
||
center_x = (bounding_box["x"] + bounding_box["width"] / 2) * scale_x + offset_x
|
||
center_y = (bounding_box["y"] + bounding_box["height"] / 2) * scale_y + offset_y
|
||
|
||
# Прокручиваем и ждем
|
||
row.scroll_into_view_if_needed()
|
||
self.page.wait_for_timeout(3000)
|
||
|
||
self.page.mouse.move(math.ceil(center_x), math.ceil(center_y), steps=5)
|
||
self.page.wait_for_timeout(3000)
|
||
# print(math.ceil(center_y))
|
||
# print(offset_y)
|
||
# print(scale_y)
|
||
|
||
# Получение текущего цвета фона
|
||
new_color = row.evaluate("el => window.getComputedStyle(el).backgroundColor")
|
||
assert initial_color != new_color, "Color of row did not change when hovering the cursor"
|