Skip to content

BaseComponent

Базовый модуль для работы с компонентами страницы.

Содержит базовый класс для взаимодействия с элементами страницы через Playwright.

BaseComponent

Базовый компонент для работы с элементами страницы.

Предоставляет общие методы для взаимодействия с элементами: - получение локаторов - проверка видимости элементов - работа с прокруткой

Source code in components\base_component.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
class BaseComponent:
    """Базовый компонент для работы с элементами страницы.

    Предоставляет общие методы для взаимодействия с элементами:
    - получение локаторов
    - проверка видимости элементов
    - работа с прокруткой
    """

    def __init__(self, page: Page):
        """Инициализация базового компонента.

        Args:
            page: экземпляр страницы Playwright.
        """

        self.page = page

    # Действия:
    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_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}"
        )

__init__(page)

Инициализация базового компонента.

Parameters:

Name Type Description Default
page Page

экземпляр страницы Playwright.

required
Source code in components\base_component.py
21
22
23
24
25
26
27
28
def __init__(self, page: Page):
    """Инициализация базового компонента.

    Args:
        page: экземпляр страницы Playwright.
    """

    self.page = page

check_absence(locator, msg)

Проверка отсутствия элемента на странице.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента (строка или объект Locator).

required
msg str

сообщение об ошибке при неудачной проверке.

required

Raises:

Type Description
AssertionError

если элемент виден на странице.

Source code in components\base_component.py
62
63
64
65
66
67
68
69
70
71
72
73
74
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

check_visibility(locator, msg)

Проверка видимости элемента на странице.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента (строка или объект Locator).

required
msg str

сообщение об ошибке при неудачной проверке.

required

Raises:

Type Description
AssertionError

если элемент не виден на странице.

Source code in components\base_component.py
76
77
78
79
80
81
82
83
84
85
86
87
88
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

get_locator(locator)

Получение объекта Locator из строки или существующего Locator.

Parameters:

Name Type Description Default
locator str | Locator

строка с CSS/XPath селектором или объект Locator.

required

Returns:

Name Type Description
Locator Locator

объект для работы с элементом.

Raises:

Type Description
TypeError

если передан некорректный тип локатора.

Source code in components\base_component.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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")

is_scrollable_horizontally(locator)

Проверка возможности горизонтальной прокрутки элемента.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента.

required

Returns:

Name Type Description
bool bool

True если элемент можно прокрутить горизонтально.

Source code in components\base_component.py
103
104
105
106
107
108
109
110
111
112
113
114
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")

is_scrollable_vertically(locator)

Проверка возможности вертикальной прокрутки элемента.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента.

required

Returns:

Name Type Description
bool bool

True если элемент можно прокрутить вертикально.

Source code in components\base_component.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
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")

scroll_down(locator, timeout=10000)

Прокрутка элемента до самого конца.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента.

required
timeout int

максимальное время ожидания в мс.

10000

Raises:

Type Description
AssertionError

если прокрутка не выполнена до конца.

Source code in components\base_component.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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}"
    )

scroll_left(locator, timeout=10000)

Прокрутка элемента до самого левого края.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента.

required
timeout int

максимальное время ожидания в мс.

10000

Raises:

Type Description
AssertionError

если прокрутка не выполнена до левого края.

Source code in components\base_component.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
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}"
    )

scroll_right(locator, timeout=10000)

Прокрутка элемента до самого правого края.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента.

required
timeout int

максимальное время ожидания в мс.

10000

Raises:

Type Description
AssertionError

если прокрутка не выполнена до правого края.

Source code in components\base_component.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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}"
    )

scroll_up(locator, timeout=10000)

Прокрутка элемента до самого верха.

Parameters:

Name Type Description Default
locator str | Locator

локатор элемента.

required
timeout int

максимальное время ожидания в мс.

10000

Raises:

Type Description
AssertionError

если прокрутка не выполнена до верха.

Source code in components\base_component.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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}"
    )