375 lines
16 KiB
Python
375 lines
16 KiB
Python
"""Базовый модуль для работы с компонентами страницы.
|
||
|
||
Содержит базовый класс для взаимодействия с элементами страницы через Playwright.
|
||
"""
|
||
|
||
from playwright.sync_api import Page, Locator, expect
|
||
from tools.logger import get_logger
|
||
|
||
logger = get_logger("BASE_COMPONENT")
|
||
|
||
|
||
class BaseComponent:
|
||
"""Базовый компонент для работы с элементами страницы.
|
||
|
||
Предоставляет общие методы для взаимодействия с элементами:
|
||
- получение локаторов
|
||
- проверка видимости элементов
|
||
- работа с прокруткой
|
||
"""
|
||
|
||
def __init__(self, page: Page):
|
||
"""Инициализация базового компонента.
|
||
|
||
Args:
|
||
page: экземпляр страницы Playwright.
|
||
"""
|
||
|
||
self.page = page
|
||
|
||
# Действия:
|
||
def get_input_fields_locators(self, container_locator: Locator, search_class: str = "layout") -> dict:
|
||
"""Получение объекта словаря имя поля ввода : Locator.
|
||
|
||
Args:
|
||
container_locator: объект Locator.
|
||
search_class: css класс для поиска (по умолчанию layout)
|
||
|
||
Returns:
|
||
dict: словарь имя поля ввода : Locator xs8 контейнера
|
||
"""
|
||
|
||
fields_locators = {}
|
||
|
||
if search_class == "layout":
|
||
# Поиск по структуре layout -> xs4 (label) + xs8 (input контейнер)
|
||
layout_containers = container_locator.locator("div.layout")
|
||
|
||
for i in range(layout_containers.count()):
|
||
layout_container = layout_containers.nth(i)
|
||
|
||
xs4_div = layout_container.locator("div.flex.xs4").first
|
||
xs8_div = layout_container.locator("div.flex.xs8").first
|
||
|
||
if xs4_div.count() > 0 and xs8_div.count() > 0:
|
||
# Ищем input в label_container
|
||
label_input = xs4_div.locator("div.v-text-field__slot > input").first
|
||
if label_input.count() > 0:
|
||
label_text = label_input.input_value().strip()
|
||
|
||
if label_text:
|
||
# Возвращаем xs8 контейнер
|
||
fields_locators[label_text] = xs8_div
|
||
|
||
return fields_locators
|
||
|
||
def get_locator(self, locator: str | Locator) -> Locator:
|
||
"""Получение объекта Locator из строки или существующего Locator.
|
||
|
||
Args:
|
||
locator: строка с CSS/XPath селектором или объект Locator.
|
||
|
||
Returns:
|
||
Locator: объект для работы с элементом.
|
||
|
||
Raises:
|
||
TypeError: если передан некорректный тип локатора.
|
||
"""
|
||
|
||
if isinstance(locator, Locator):
|
||
return locator
|
||
elif isinstance(locator, str):
|
||
return self.page.locator(locator)
|
||
else:
|
||
raise TypeError("locator value should be string type or Locator type")
|
||
|
||
def wait_for_timeout(self, timeout: int) -> None:
|
||
"""
|
||
Ожидает указанное количество миллисекунд.
|
||
|
||
Args:
|
||
timeout: Время ожидания в миллисекундах
|
||
"""
|
||
self.page.wait_for_timeout(timeout)
|
||
|
||
# Закомментированный код сохранен без изменений
|
||
# def wait_for_all_elements(self, locator: Locator, timeout=5000):
|
||
# loc = self.get_locator(locator)
|
||
# elements = self.page.locator(loc).all()
|
||
#
|
||
# for element in elements:
|
||
# self.page.locator(loc).wait_for(timeout=timeout)
|
||
#
|
||
# return elements
|
||
|
||
# Проверки:
|
||
def check_absence(self, locator: str | Locator, msg: str) -> None:
|
||
"""Проверка отсутствия элемента на странице.
|
||
|
||
Args:
|
||
locator: локатор элемента (строка или объект Locator).
|
||
msg: сообщение об ошибке при неудачной проверке.
|
||
|
||
Raises:
|
||
AssertionError: если элемент виден на странице.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
expect(loc).to_be_hidden(timeout=12000), msg
|
||
|
||
def check_visibility(self, locator: str | Locator, msg: str) -> None:
|
||
"""Проверка видимости элемента на странице.
|
||
|
||
Args:
|
||
locator: локатор элемента (строка или объект Locator).
|
||
msg: сообщение об ошибке при неудачной проверке.
|
||
|
||
Raises:
|
||
AssertionError: если элемент не виден на странице.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
expect(loc).to_be_visible(visible=True, timeout=12000), msg
|
||
|
||
def is_scrollable_vertically(self, locator: str | Locator) -> bool:
|
||
"""Проверка возможности вертикальной прокрутки элемента.
|
||
|
||
Args:
|
||
locator: локатор элемента.
|
||
|
||
Returns:
|
||
bool: True если элемент можно прокрутить вертикально.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
return loc.evaluate("el => el.scrollHeight > el.clientHeight")
|
||
|
||
def is_scrollable_horizontally(self, locator: str | Locator) -> bool:
|
||
"""Проверка возможности горизонтальной прокрутки элемента.
|
||
|
||
Args:
|
||
locator: локатор элемента.
|
||
|
||
Returns:
|
||
bool: True если элемент можно прокрутить горизонтально.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
return loc.evaluate("el => el.scrollWidth > el.clientWidth")
|
||
|
||
# Методы прокрутки:
|
||
def scroll_up(self, locator: str | Locator, timeout: int = 10000) -> None:
|
||
"""Прокрутка элемента до самого верха.
|
||
|
||
Args:
|
||
locator: локатор элемента.
|
||
timeout: максимальное время ожидания в мс.
|
||
|
||
Raises:
|
||
AssertionError: если прокрутка не выполнена до верха.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
max_attempts = 3
|
||
attempt = 0
|
||
|
||
while attempt < max_attempts:
|
||
# Получаем текущую позицию прокрутки
|
||
previous_position = loc.evaluate("el => el.scrollTop")
|
||
|
||
# Прокручиваем до верха
|
||
loc.evaluate("el => el.scrollTo(0, 0)")
|
||
|
||
# Ждем завершения прокрутки с использованием timeout
|
||
loc.wait_for(timeout=min(2000, timeout))
|
||
|
||
# Получаем новую позицию прокрутки
|
||
current_position = loc.evaluate("el => el.scrollTop")
|
||
|
||
# Проверяем, достигли ли мы верха
|
||
if current_position <= 1: # <= 1 для учета погрешности
|
||
return # Успешно прокрутили до верха
|
||
|
||
# Проверяем, была ли вообще прокрутка
|
||
if current_position >= previous_position:
|
||
attempt += 1
|
||
else:
|
||
attempt = 0 # Сбрасываем счетчик, если была прокрутка
|
||
|
||
# Если вышли из цикла - не смогли прокрутить до верха
|
||
final_position = loc.evaluate("el => el.scrollTop")
|
||
scroll_height = loc.evaluate("el => el.scrollHeight")
|
||
client_height = loc.evaluate("el => el.clientHeight")
|
||
|
||
raise AssertionError(
|
||
f"Не удалось прокрутить до верха. "
|
||
f"Текущая позиция: {final_position}, ожидалось: 0, "
|
||
f"Высота контента: {scroll_height}, "
|
||
f"Высота видимой области: {client_height}"
|
||
)
|
||
|
||
def scroll_down(self, locator: str | Locator, timeout: int = 10000) -> None:
|
||
"""Прокрутка элемента до самого конца.
|
||
|
||
Args:
|
||
locator: локатор элемента.
|
||
timeout: максимальное время ожидания в мс.
|
||
|
||
Raises:
|
||
AssertionError: если прокрутка не выполнена до конца.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
max_attempts = 3
|
||
attempt = 0
|
||
|
||
while attempt < max_attempts:
|
||
# Получаем текущую позицию прокрутки и размеры
|
||
current_position = loc.evaluate("el => el.scrollTop")
|
||
scroll_height = loc.evaluate("el => el.scrollHeight")
|
||
client_height = loc.evaluate("el => el.clientHeight")
|
||
|
||
# Проверяем, достигли ли мы конца ДО прокрутки
|
||
if current_position + client_height >= scroll_height - 5: # -5 для учета погрешности
|
||
return # Уже прокручено до конца
|
||
|
||
# Прокручиваем до конца
|
||
loc.evaluate("el => el.scrollTo(0, el.scrollHeight)")
|
||
|
||
# Ждем загрузки контента
|
||
loc.wait_for(timeout=min(2000, timeout))
|
||
|
||
# Получаем новую позицию прокрутки
|
||
new_position = loc.evaluate("el => el.scrollTop")
|
||
new_scroll_height = loc.evaluate("el => el.scrollHeight")
|
||
new_client_height = loc.evaluate("el => el.clientHeight")
|
||
|
||
# Проверяем, достигли ли мы конца
|
||
if new_position + new_client_height >= new_scroll_height - 5: # -5 для учета погрешности
|
||
return # Успешно прокрутили до конца
|
||
|
||
# Проверяем, была ли прокрутка
|
||
if new_position <= current_position and new_scroll_height <= scroll_height:
|
||
# Прокрутки не произошло или контент не изменился
|
||
attempt += 1
|
||
else:
|
||
attempt = 0 # Сбрасываем счетчик, если была прокрутка
|
||
|
||
# Если вышли из цикла - не смогли прокрутить до конца
|
||
final_position = loc.evaluate("el => el.scrollTop")
|
||
final_scroll_height = loc.evaluate("el => el.scrollHeight")
|
||
final_client_height = loc.evaluate("el => el.clientHeight")
|
||
max_scroll_y = final_scroll_height - final_client_height
|
||
|
||
raise AssertionError(
|
||
f"Не удалось прокрутить до конца. "
|
||
f"Текущая позиция: {final_position}, ожидалось: {max_scroll_y}, "
|
||
f"Высота контента: {final_scroll_height}, "
|
||
f"Высота видимой области: {final_client_height}, "
|
||
f"Сумма: {final_position + final_client_height}"
|
||
)
|
||
|
||
def scroll_left(self, locator: str | Locator, timeout: int = 10000) -> None:
|
||
"""Прокрутка элемента до самого левого края.
|
||
|
||
Args:
|
||
locator: локатор элемента.
|
||
timeout: максимальное время ожидания в мс.
|
||
|
||
Raises:
|
||
AssertionError: если прокрутка не выполнена до левого края.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
max_attempts = 3
|
||
attempt = 0
|
||
|
||
while attempt < max_attempts:
|
||
# Получаем текущую позицию прокрутки
|
||
previous_position = loc.evaluate("el => el.scrollLeft")
|
||
|
||
# Прокручиваем до левого края
|
||
loc.evaluate("el => el.scrollTo(0, el.scrollTop)")
|
||
|
||
# Ждем завершения прокрутки с использованием timeout
|
||
loc.wait_for(timeout=min(2000, timeout))
|
||
|
||
# Получаем новую позицию прокрутки
|
||
current_position = loc.evaluate("el => el.scrollLeft")
|
||
|
||
# Проверяем, достигли ли мы левого края
|
||
if current_position <= 1: # <= 1 для учета погрешности
|
||
return # Успешно прокрутили до левого края
|
||
|
||
# Проверяем, была ли вообще прокрутка
|
||
if current_position >= previous_position:
|
||
attempt += 1
|
||
else:
|
||
attempt = 0 # Сбрасываем счетчик, если была прокрутка
|
||
|
||
# Если вышли из цикла - не смогли прокрутить до левого края
|
||
final_position = loc.evaluate("el => el.scrollLeft")
|
||
scroll_width = loc.evaluate("el => el.scrollWidth")
|
||
client_width = loc.evaluate("el => el.clientWidth")
|
||
|
||
raise AssertionError(
|
||
f"Не удалось прокрутить до левого края. "
|
||
f"Текущая позиция: {final_position}, ожидалось: 0, "
|
||
f"Ширина контента: {scroll_width}, "
|
||
f"Ширина видимой области: {client_width}"
|
||
)
|
||
|
||
def scroll_right(self, locator: str | Locator, timeout: int = 10000) -> None:
|
||
"""Прокрутка элемента до самого правого края.
|
||
|
||
Args:
|
||
locator: локатор элемента.
|
||
timeout: максимальное время ожидания в мс.
|
||
|
||
Raises:
|
||
AssertionError: если прокрутка не выполнена до правого края.
|
||
"""
|
||
|
||
loc = self.get_locator(locator)
|
||
max_attempts = 3
|
||
attempt = 0
|
||
|
||
while attempt < max_attempts:
|
||
# Получаем текущую позицию прокрутки
|
||
previous_position = loc.evaluate("el => el.scrollLeft")
|
||
|
||
# Прокручиваем до правого края
|
||
loc.evaluate("el => el.scrollTo(el.scrollWidth, el.scrollTop)")
|
||
|
||
# Ждем завершения прокрутки с использованием timeout
|
||
loc.wait_for(timeout=min(2000, timeout))
|
||
|
||
# Получаем новую позицию прокрутки
|
||
current_position = loc.evaluate("el => el.scrollLeft")
|
||
scroll_width = loc.evaluate("el => el.scrollWidth")
|
||
client_width = loc.evaluate("el => el.clientWidth")
|
||
|
||
# Проверяем, достигли ли мы правого края
|
||
max_scroll_x = scroll_width - client_width
|
||
if current_position >= max_scroll_x - 1: # -1 для учета погрешности
|
||
return # Успешно прокрутили до правого края
|
||
|
||
# Проверяем, была ли вообще прокрутка
|
||
if current_position <= previous_position:
|
||
attempt += 1
|
||
else:
|
||
attempt = 0 # Сбрасываем счетчик, если была прокрутка
|
||
|
||
# Если вышли из цикла - не смогли прокрутить до правого края
|
||
final_position = loc.evaluate("el => el.scrollLeft")
|
||
scroll_width = loc.evaluate("el => el.scrollWidth")
|
||
client_width = loc.evaluate("el => el.clientWidth")
|
||
max_scroll_x = scroll_width - client_width
|
||
|
||
raise AssertionError(
|
||
f"Не удалось прокрутить до правого края. "
|
||
f"Текущая позиция: {final_position}, ожидалось: {max_scroll_x}, "
|
||
f"Ширина контента: {scroll_width}, "
|
||
f"Ширина видимой области: {client_width}"
|
||
)
|