persona-dsl 26.1.21.44__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- persona_dsl/__init__.py +35 -0
- persona_dsl/components/action.py +10 -0
- persona_dsl/components/base_step.py +251 -0
- persona_dsl/components/combined_step.py +68 -0
- persona_dsl/components/expectation.py +10 -0
- persona_dsl/components/fact.py +10 -0
- persona_dsl/components/goal.py +10 -0
- persona_dsl/components/ops.py +7 -0
- persona_dsl/components/step.py +75 -0
- persona_dsl/expectations/generic/__init__.py +15 -0
- persona_dsl/expectations/generic/contains_item.py +19 -0
- persona_dsl/expectations/generic/contains_the_text.py +15 -0
- persona_dsl/expectations/generic/has_entries.py +21 -0
- persona_dsl/expectations/generic/is_equal.py +24 -0
- persona_dsl/expectations/generic/is_greater_than.py +18 -0
- persona_dsl/expectations/generic/path_equal.py +27 -0
- persona_dsl/expectations/web/__init__.py +5 -0
- persona_dsl/expectations/web/is_displayed.py +13 -0
- persona_dsl/expectations/web/matches_aria_snapshot.py +222 -0
- persona_dsl/expectations/web/matches_screenshot.py +160 -0
- persona_dsl/generators/__init__.py +5 -0
- persona_dsl/generators/api_generator.py +423 -0
- persona_dsl/generators/cli.py +431 -0
- persona_dsl/generators/page_generator.py +1140 -0
- persona_dsl/ops/api/__init__.py +5 -0
- persona_dsl/ops/api/json_as.py +104 -0
- persona_dsl/ops/api/json_response.py +48 -0
- persona_dsl/ops/api/send_request.py +41 -0
- persona_dsl/ops/db/__init__.py +5 -0
- persona_dsl/ops/db/execute_sql.py +22 -0
- persona_dsl/ops/db/fetch_all.py +29 -0
- persona_dsl/ops/db/fetch_one.py +22 -0
- persona_dsl/ops/kafka/__init__.py +4 -0
- persona_dsl/ops/kafka/message_in_topic.py +89 -0
- persona_dsl/ops/kafka/send_message.py +35 -0
- persona_dsl/ops/soap/__init__.py +4 -0
- persona_dsl/ops/soap/call_operation.py +24 -0
- persona_dsl/ops/soap/operation_result.py +24 -0
- persona_dsl/ops/web/__init__.py +37 -0
- persona_dsl/ops/web/aria_snapshot.py +87 -0
- persona_dsl/ops/web/click.py +30 -0
- persona_dsl/ops/web/current_path.py +17 -0
- persona_dsl/ops/web/element_attribute.py +24 -0
- persona_dsl/ops/web/element_is_visible.py +27 -0
- persona_dsl/ops/web/element_text.py +28 -0
- persona_dsl/ops/web/elements_count.py +42 -0
- persona_dsl/ops/web/fill.py +41 -0
- persona_dsl/ops/web/generate_page_object.py +118 -0
- persona_dsl/ops/web/input_value.py +23 -0
- persona_dsl/ops/web/navigate.py +52 -0
- persona_dsl/ops/web/press_key.py +37 -0
- persona_dsl/ops/web/rich_aria_snapshot.py +159 -0
- persona_dsl/ops/web/screenshot.py +68 -0
- persona_dsl/ops/web/table_data.py +43 -0
- persona_dsl/ops/web/wait_for_navigation.py +23 -0
- persona_dsl/pages/__init__.py +133 -0
- persona_dsl/pages/elements.py +998 -0
- persona_dsl/pages/page.py +44 -0
- persona_dsl/pages/virtual_page.py +94 -0
- persona_dsl/persona.py +125 -0
- persona_dsl/pytest_plugin.py +1230 -0
- persona_dsl/runtime/dist/persona_bundle.js +1077 -0
- persona_dsl/skills/__init__.py +7 -0
- persona_dsl/skills/core/base.py +41 -0
- persona_dsl/skills/core/skill_definition.py +30 -0
- persona_dsl/skills/use_api.py +251 -0
- persona_dsl/skills/use_browser.py +78 -0
- persona_dsl/skills/use_database.py +129 -0
- persona_dsl/skills/use_kafka.py +135 -0
- persona_dsl/skills/use_soap.py +66 -0
- persona_dsl/utils/__init__.py +0 -0
- persona_dsl/utils/artifacts.py +22 -0
- persona_dsl/utils/config.py +54 -0
- persona_dsl/utils/data_providers.py +159 -0
- persona_dsl/utils/decorators.py +80 -0
- persona_dsl/utils/metrics.py +69 -0
- persona_dsl/utils/naming.py +14 -0
- persona_dsl/utils/path.py +202 -0
- persona_dsl/utils/retry.py +51 -0
- persona_dsl/utils/taas_integration.py +124 -0
- persona_dsl/utils/waits.py +112 -0
- persona_dsl-26.1.21.44.dist-info/METADATA +233 -0
- persona_dsl-26.1.21.44.dist-info/RECORD +86 -0
- persona_dsl-26.1.21.44.dist-info/WHEEL +5 -0
- persona_dsl-26.1.21.44.dist-info/entry_points.txt +6 -0
- persona_dsl-26.1.21.44.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, List, Optional, Type, Iterator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Strategy(str, Enum):
|
|
10
|
+
"""Стратегии поиска элемента."""
|
|
11
|
+
|
|
12
|
+
TEST_ID = "test_id"
|
|
13
|
+
ROLE = "role"
|
|
14
|
+
TEXT = "text"
|
|
15
|
+
PLACEHOLDER = "placeholder"
|
|
16
|
+
ALT_TEXT = "alt_text"
|
|
17
|
+
TITLE = "title"
|
|
18
|
+
LOCATOR = "locator" # css/xpath
|
|
19
|
+
REF = "ref" # data-persona-id (Highest Priority with Runtime)
|
|
20
|
+
LINK = "link" # href
|
|
21
|
+
ALL = "all" # Использовать все доступные
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Element:
|
|
26
|
+
"""
|
|
27
|
+
Базовый класс для элементов страницы с поддержкой Multi-Strategy Resolution и Hybrid Navigation.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
role: Optional[str] = None
|
|
32
|
+
accessible_name: Optional[str] = None
|
|
33
|
+
text: Optional[str | Any] = None
|
|
34
|
+
label: Optional[str] = None
|
|
35
|
+
placeholder: Optional[str] = None
|
|
36
|
+
test_id: Optional[str] = None
|
|
37
|
+
alt_text: Optional[str] = None
|
|
38
|
+
title: Optional[str] = None
|
|
39
|
+
locator: Optional[str] = None
|
|
40
|
+
index: Optional[int] = None
|
|
41
|
+
exact: bool = False
|
|
42
|
+
description: Optional[str] = None
|
|
43
|
+
static_screenshot_path: Optional[str] = None
|
|
44
|
+
static_aria_snapshot_path: Optional[str] = None
|
|
45
|
+
aria_ref: Optional[str] = None
|
|
46
|
+
url: Optional[str] = None # Для Strategy.LINK
|
|
47
|
+
|
|
48
|
+
_filters: List[Dict[str, Any]] = field(default_factory=list, repr=False, init=False)
|
|
49
|
+
# Список стратегий поиска (если None — используется стандартный приоритет)
|
|
50
|
+
_strategies: Optional[List[Strategy]] = field(default=None, repr=False, init=False)
|
|
51
|
+
parent: Optional["Element"] = field(default=None, repr=False, init=False)
|
|
52
|
+
|
|
53
|
+
# Навигационные поля
|
|
54
|
+
_nav_source: Optional["Element"] = field(default=None, repr=False, init=False)
|
|
55
|
+
_nav_type: Optional[str] = field(
|
|
56
|
+
default=None, repr=False, init=False
|
|
57
|
+
) # 'child', 'next', 'prev', 'parent'
|
|
58
|
+
_nav_arg: Optional[Any] = field(default=None, repr=False, init=False)
|
|
59
|
+
|
|
60
|
+
# Кэш для Smart Shortcuts
|
|
61
|
+
_shortcut_cache: Dict[str, "Element"] = field(
|
|
62
|
+
default_factory=dict, repr=False, init=False
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
self._elements: Optional[Dict[str, Element | ElementList]] = None
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_container(self) -> bool:
|
|
70
|
+
return self._elements is not None
|
|
71
|
+
|
|
72
|
+
def using(self, strategies: List[Strategy]) -> "Element":
|
|
73
|
+
"""
|
|
74
|
+
Возвращает копию элемента с заданным набором стратегий поиска.
|
|
75
|
+
"""
|
|
76
|
+
new_element = copy.copy(self)
|
|
77
|
+
new_element._strategies = strategies
|
|
78
|
+
new_element._filters = list(self._filters)
|
|
79
|
+
return new_element
|
|
80
|
+
|
|
81
|
+
def robust(self) -> "Element":
|
|
82
|
+
"""Алиас для использования всех доступных стратегий (Strategy.ALL)."""
|
|
83
|
+
return self.using([Strategy.ALL])
|
|
84
|
+
|
|
85
|
+
def __call__(self, text: str | None = None, **kwargs: Any) -> "Element":
|
|
86
|
+
"""
|
|
87
|
+
Возвращает копию элемента с модифицированными атрибутами.
|
|
88
|
+
"""
|
|
89
|
+
new_element = copy.copy(self)
|
|
90
|
+
new_element._filters = list(self._filters)
|
|
91
|
+
|
|
92
|
+
for k, v in kwargs.items():
|
|
93
|
+
if hasattr(new_element, k):
|
|
94
|
+
setattr(new_element, k, v)
|
|
95
|
+
|
|
96
|
+
if text is not None:
|
|
97
|
+
if self.role and self.role != "generic":
|
|
98
|
+
new_element.accessible_name = text
|
|
99
|
+
else:
|
|
100
|
+
new_element.text = text
|
|
101
|
+
|
|
102
|
+
return new_element
|
|
103
|
+
|
|
104
|
+
# --- Structural Navigation Methods ---
|
|
105
|
+
|
|
106
|
+
def _create_nav_element(self, nav_type: str, arg: Any = None) -> "Element":
|
|
107
|
+
"""Создает новый элемент, привязанный к текущему через навигацию."""
|
|
108
|
+
# Создаем generic элемент, так как мы не знаем роль цели заранее
|
|
109
|
+
name_suffix = f"_{nav_type}"
|
|
110
|
+
if arg is not None:
|
|
111
|
+
name_suffix += f"_{arg}"
|
|
112
|
+
|
|
113
|
+
new_el = Element(name=f"{self.name}{name_suffix}", role="generic")
|
|
114
|
+
new_el.parent = self # Важно для цепочки resolve
|
|
115
|
+
new_el._nav_source = self
|
|
116
|
+
new_el._nav_type = nav_type
|
|
117
|
+
new_el._nav_arg = arg
|
|
118
|
+
return new_el
|
|
119
|
+
|
|
120
|
+
def child(self, index: int = 0) -> "Element":
|
|
121
|
+
"""Возвращает n-го ребенка элемента."""
|
|
122
|
+
return self._create_nav_element("child", index)
|
|
123
|
+
|
|
124
|
+
def first_child(self) -> "Element":
|
|
125
|
+
"""Возвращает первого ребенка."""
|
|
126
|
+
return self.child(0)
|
|
127
|
+
|
|
128
|
+
def last_child(self) -> "Element":
|
|
129
|
+
"""Возвращает последнего ребенка."""
|
|
130
|
+
return self._create_nav_element("last_child")
|
|
131
|
+
|
|
132
|
+
def next_sibling(self) -> "Element":
|
|
133
|
+
"""Возвращает следующий элемент на том же уровне."""
|
|
134
|
+
return self._create_nav_element("next")
|
|
135
|
+
|
|
136
|
+
def prev_sibling(self) -> "Element":
|
|
137
|
+
"""Возвращает предыдущий элемент на том же уровне."""
|
|
138
|
+
return self._create_nav_element("prev")
|
|
139
|
+
|
|
140
|
+
def parent_element(self) -> "Element":
|
|
141
|
+
"""Возвращает родительский элемент (DOM parent)."""
|
|
142
|
+
return self._create_nav_element("parent")
|
|
143
|
+
|
|
144
|
+
# --- Resolution Logic ---
|
|
145
|
+
|
|
146
|
+
def _resolve_single_strategy(self, base: Any, strategy: Strategy) -> Any:
|
|
147
|
+
"""Строит локатор для одной конкретной стратегии."""
|
|
148
|
+
if strategy == Strategy.REF and self.aria_ref:
|
|
149
|
+
return base.locator(f'[data-persona-id="{self.aria_ref}"]')
|
|
150
|
+
|
|
151
|
+
if strategy == Strategy.LINK and self.url:
|
|
152
|
+
# Ищем ссылку с частичным совпадением href
|
|
153
|
+
return base.locator(f'a[href*="{self.url}"]')
|
|
154
|
+
|
|
155
|
+
if strategy == Strategy.TEST_ID and self.test_id:
|
|
156
|
+
return base.get_by_test_id(self.test_id)
|
|
157
|
+
|
|
158
|
+
if strategy == Strategy.ROLE and self.role:
|
|
159
|
+
if self.accessible_name:
|
|
160
|
+
return base.get_by_role(
|
|
161
|
+
self.role, name=self.accessible_name, exact=self.exact
|
|
162
|
+
)
|
|
163
|
+
return base.get_by_role(self.role, exact=self.exact)
|
|
164
|
+
|
|
165
|
+
if strategy == Strategy.TEXT and self.text:
|
|
166
|
+
return base.get_by_text(self.text, exact=self.exact)
|
|
167
|
+
|
|
168
|
+
if strategy == Strategy.PLACEHOLDER and self.placeholder:
|
|
169
|
+
return base.get_by_placeholder(self.placeholder, exact=self.exact)
|
|
170
|
+
|
|
171
|
+
if strategy == Strategy.ALT_TEXT and self.alt_text:
|
|
172
|
+
return base.get_by_alt_text(self.alt_text, exact=self.exact)
|
|
173
|
+
|
|
174
|
+
if strategy == Strategy.TITLE and self.title:
|
|
175
|
+
return base.get_by_title(self.title, exact=self.exact)
|
|
176
|
+
|
|
177
|
+
if strategy == Strategy.LOCATOR and self.locator:
|
|
178
|
+
return base.locator(self.locator)
|
|
179
|
+
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def _resolve_navigation_step(self, base: Any) -> Any:
|
|
183
|
+
"""
|
|
184
|
+
Гибридный резолвер навигации.
|
|
185
|
+
Пытается использовать Runtime JS, иначе падает в CSS/XPath.
|
|
186
|
+
"""
|
|
187
|
+
page = base.page
|
|
188
|
+
|
|
189
|
+
# 1. Hybrid Path: Try JS Runtime if parent has ref
|
|
190
|
+
if self._nav_source and self._nav_source.aria_ref:
|
|
191
|
+
# Проверяем наличие Runtime (быстрая проверка через evaluate handle или просто try/catch в JS)
|
|
192
|
+
# Используем evaluate для атомарности
|
|
193
|
+
js_script = """
|
|
194
|
+
([ref, type, arg]) => {
|
|
195
|
+
if (!window.__PERSONA__) return null;
|
|
196
|
+
const el = window.__PERSONA__.navigate(ref, type, arg);
|
|
197
|
+
return el ? el.getAttribute('data-persona-id') : null;
|
|
198
|
+
}
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
target_id = page.evaluate(
|
|
202
|
+
js_script,
|
|
203
|
+
[self._nav_source.aria_ref, self._nav_type, self._nav_arg],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if target_id:
|
|
207
|
+
# Если JS нашел элемент и вернул ID -> строим прямой локатор
|
|
208
|
+
# Важно: ищем от корня страницы, так как ID уникален
|
|
209
|
+
return page.locator(f'[data-persona-id="{target_id}"]')
|
|
210
|
+
except Exception:
|
|
211
|
+
# Если JS упал или Runtime нет - игнорируем и идем в fallback
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# 2. Pure Path: CSS/XPath Fallback
|
|
215
|
+
if self._nav_type == "child":
|
|
216
|
+
# Playwright/CSS nth-child is 1-based
|
|
217
|
+
# :scope > * выбирает всех детей
|
|
218
|
+
return base.locator(":scope > *").nth(self._nav_arg)
|
|
219
|
+
|
|
220
|
+
if self._nav_type == "last_child":
|
|
221
|
+
return base.locator(":scope > *").last
|
|
222
|
+
|
|
223
|
+
if self._nav_type == "next":
|
|
224
|
+
return base.locator("xpath=following-sibling::*[1]")
|
|
225
|
+
|
|
226
|
+
if self._nav_type == "prev":
|
|
227
|
+
return base.locator("xpath=preceding-sibling::*[1]")
|
|
228
|
+
|
|
229
|
+
if self._nav_type == "parent":
|
|
230
|
+
return base.locator("xpath=..")
|
|
231
|
+
|
|
232
|
+
raise ValueError(f"Unknown navigation type: {self._nav_type}")
|
|
233
|
+
|
|
234
|
+
def _get_playwright_locator(self, base: Any) -> Any:
|
|
235
|
+
"""Получение локатора с поддержкой Multi-Strategy и Navigation."""
|
|
236
|
+
|
|
237
|
+
# 0. Navigation Step
|
|
238
|
+
if self._nav_type:
|
|
239
|
+
return self._resolve_navigation_step(base)
|
|
240
|
+
|
|
241
|
+
# 1. Сбор локаторов
|
|
242
|
+
locators = []
|
|
243
|
+
|
|
244
|
+
# Приоритет REF стратегии, если есть aria_ref (LCL-47)
|
|
245
|
+
# Если стратегии не заданы явно, добавляем REF в начало списка приоритетов
|
|
246
|
+
strategies_to_check = []
|
|
247
|
+
|
|
248
|
+
if self._strategies:
|
|
249
|
+
if Strategy.ALL in self._strategies:
|
|
250
|
+
strategies_to_check = [
|
|
251
|
+
Strategy.REF,
|
|
252
|
+
Strategy.TEST_ID,
|
|
253
|
+
Strategy.ROLE,
|
|
254
|
+
Strategy.PLACEHOLDER,
|
|
255
|
+
Strategy.ALT_TEXT,
|
|
256
|
+
Strategy.TITLE,
|
|
257
|
+
Strategy.TEXT,
|
|
258
|
+
Strategy.LINK,
|
|
259
|
+
Strategy.LOCATOR,
|
|
260
|
+
]
|
|
261
|
+
else:
|
|
262
|
+
strategies_to_check = self._strategies
|
|
263
|
+
else:
|
|
264
|
+
# Стандартный приоритет
|
|
265
|
+
# REF > TEST_ID > ROLE ...
|
|
266
|
+
strategies_to_check = []
|
|
267
|
+
if self.aria_ref:
|
|
268
|
+
strategies_to_check.append(Strategy.REF)
|
|
269
|
+
if self.test_id:
|
|
270
|
+
strategies_to_check.append(Strategy.TEST_ID)
|
|
271
|
+
if self.role:
|
|
272
|
+
strategies_to_check.append(Strategy.ROLE)
|
|
273
|
+
if self.label:
|
|
274
|
+
# Label strategy is not in Enum yet, handled via get_by_label below if needed,
|
|
275
|
+
# or we can add it. For now keeping legacy fallback logic structure if strategies not explicit.
|
|
276
|
+
pass
|
|
277
|
+
if self.placeholder:
|
|
278
|
+
strategies_to_check.append(Strategy.PLACEHOLDER)
|
|
279
|
+
if self.alt_text:
|
|
280
|
+
strategies_to_check.append(Strategy.ALT_TEXT)
|
|
281
|
+
if self.title:
|
|
282
|
+
strategies_to_check.append(Strategy.TITLE)
|
|
283
|
+
if self.text:
|
|
284
|
+
strategies_to_check.append(Strategy.TEXT)
|
|
285
|
+
if self.url:
|
|
286
|
+
strategies_to_check.append(Strategy.LINK)
|
|
287
|
+
if self.locator:
|
|
288
|
+
strategies_to_check.append(Strategy.LOCATOR)
|
|
289
|
+
|
|
290
|
+
# Если список стратегий сформирован (явный или неявный)
|
|
291
|
+
if strategies_to_check:
|
|
292
|
+
for strat in strategies_to_check:
|
|
293
|
+
loc = self._resolve_single_strategy(base, strat)
|
|
294
|
+
if loc is not None:
|
|
295
|
+
locators.append(loc)
|
|
296
|
+
else:
|
|
297
|
+
# Fallback для label (так как его нет в Strategy Enum пока) и дефолтов
|
|
298
|
+
if self.label:
|
|
299
|
+
locators.append(base.get_by_label(self.label, exact=self.exact))
|
|
300
|
+
|
|
301
|
+
# Если совсем ничего нет
|
|
302
|
+
if not locators:
|
|
303
|
+
if self.parent is None:
|
|
304
|
+
return base
|
|
305
|
+
raise ValueError(f"Элемент '{self.name}' не имеет стратегии поиска.")
|
|
306
|
+
|
|
307
|
+
# 2. Объединение локаторов (OR)
|
|
308
|
+
if not locators:
|
|
309
|
+
raise ValueError(f"Element '{self.name}': не удалось построить локатор.")
|
|
310
|
+
|
|
311
|
+
final_locator = locators[0]
|
|
312
|
+
for loc in locators[1:]:
|
|
313
|
+
final_locator = final_locator.or_(loc)
|
|
314
|
+
|
|
315
|
+
# 3. Применение фильтров
|
|
316
|
+
if hasattr(self, "_filters") and self._filters:
|
|
317
|
+
for filter_kwargs in self._filters:
|
|
318
|
+
f_kwargs = filter_kwargs.copy()
|
|
319
|
+
if "has" in f_kwargs and isinstance(f_kwargs["has"], Element):
|
|
320
|
+
f_kwargs["has"] = f_kwargs["has"].resolve(final_locator.page)
|
|
321
|
+
if "has_not" in f_kwargs and isinstance(f_kwargs["has_not"], Element):
|
|
322
|
+
f_kwargs["has_not"] = f_kwargs["has_not"].resolve(
|
|
323
|
+
final_locator.page
|
|
324
|
+
)
|
|
325
|
+
final_locator = final_locator.filter(**f_kwargs)
|
|
326
|
+
|
|
327
|
+
# 4. Индекс
|
|
328
|
+
if self.index is not None:
|
|
329
|
+
idx = self.index
|
|
330
|
+
if idx < 0:
|
|
331
|
+
total = final_locator.count()
|
|
332
|
+
idx = total + idx
|
|
333
|
+
# Note: Playwright nth() handles negatives? No, it throws if out of bounds usually or wraps?
|
|
334
|
+
# Playwright nth() argument is 0-based index. Negative values like -1 mean last.
|
|
335
|
+
# So we can pass negative directly to nth()!
|
|
336
|
+
# But let's keep explicit logic if we want safety.
|
|
337
|
+
# Actually playwright .nth(-1) works.
|
|
338
|
+
final_locator = final_locator.nth(idx)
|
|
339
|
+
|
|
340
|
+
return final_locator
|
|
341
|
+
|
|
342
|
+
def resolve(self, page: Any) -> Any:
|
|
343
|
+
"""Вернуть Playwright Locator."""
|
|
344
|
+
chain: List[Element] = [self]
|
|
345
|
+
curr = self.parent
|
|
346
|
+
while curr is not None:
|
|
347
|
+
chain.append(curr)
|
|
348
|
+
curr = curr.parent
|
|
349
|
+
|
|
350
|
+
locator = page
|
|
351
|
+
for element in reversed(chain):
|
|
352
|
+
# Removed: if element.parent is None: continue
|
|
353
|
+
# Logic handled in _get_playwright_locator (returns base if no strategy & parent is None)
|
|
354
|
+
locator = element._get_playwright_locator(locator)
|
|
355
|
+
|
|
356
|
+
return locator
|
|
357
|
+
|
|
358
|
+
def resolve_or(self, page: Any, *alternatives: "Element") -> Any:
|
|
359
|
+
"""
|
|
360
|
+
Построить локатор как OR от self и альтернативных элементов.
|
|
361
|
+
"""
|
|
362
|
+
loc = self.resolve(page)
|
|
363
|
+
for alt in alternatives:
|
|
364
|
+
loc = loc.or_(alt.resolve(page))
|
|
365
|
+
return loc
|
|
366
|
+
|
|
367
|
+
def filter(
|
|
368
|
+
self,
|
|
369
|
+
has_text: Optional[str | Any] = None,
|
|
370
|
+
has_not_text: Optional[str | Any] = None,
|
|
371
|
+
has: Optional["Element" | Any] = None,
|
|
372
|
+
has_not: Optional["Element" | Any] = None,
|
|
373
|
+
) -> "Element":
|
|
374
|
+
new_element = copy.copy(self)
|
|
375
|
+
new_element._filters = list(self._filters)
|
|
376
|
+
|
|
377
|
+
filter_kwargs = {}
|
|
378
|
+
if has_text is not None:
|
|
379
|
+
filter_kwargs["has_text"] = has_text
|
|
380
|
+
if has_not_text is not None:
|
|
381
|
+
filter_kwargs["has_not_text"] = has_not_text
|
|
382
|
+
if has is not None:
|
|
383
|
+
filter_kwargs["has"] = has
|
|
384
|
+
if has_not is not None:
|
|
385
|
+
filter_kwargs["has_not"] = has_not
|
|
386
|
+
|
|
387
|
+
if filter_kwargs:
|
|
388
|
+
new_element._filters.append(filter_kwargs)
|
|
389
|
+
|
|
390
|
+
return new_element
|
|
391
|
+
|
|
392
|
+
# --- Container methods ---
|
|
393
|
+
|
|
394
|
+
def add_element(
|
|
395
|
+
self, element: "Element", *, alias: str | None = None, overwrite: bool = False
|
|
396
|
+
) -> "Element":
|
|
397
|
+
if self._elements is None:
|
|
398
|
+
self._elements = {}
|
|
399
|
+
|
|
400
|
+
name = alias or element.name
|
|
401
|
+
if not overwrite and (
|
|
402
|
+
(self._elements is not None and name in self._elements)
|
|
403
|
+
or hasattr(self, name)
|
|
404
|
+
):
|
|
405
|
+
raise ValueError(
|
|
406
|
+
f"Элемент/атрибут '{name}' уже существует в контейнере '{self.name}'"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
element.parent = self
|
|
410
|
+
self._elements[name] = element
|
|
411
|
+
setattr(self, name, element)
|
|
412
|
+
return element
|
|
413
|
+
|
|
414
|
+
def add_element_list(
|
|
415
|
+
self, prototype: "Element", *, alias: str | None = None, overwrite: bool = False
|
|
416
|
+
) -> "ElementList":
|
|
417
|
+
if self._elements is None:
|
|
418
|
+
self._elements = {}
|
|
419
|
+
|
|
420
|
+
list_name = alias or f"{prototype.name}s"
|
|
421
|
+
if not overwrite and (
|
|
422
|
+
(self._elements is not None and list_name in self._elements)
|
|
423
|
+
or hasattr(self, list_name)
|
|
424
|
+
):
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f"Элемент/атрибут '{list_name}' уже существует в контейнере '{self.name}'"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
el_list = ElementList(owner=self, prototype=prototype, name=list_name)
|
|
430
|
+
self._elements[list_name] = el_list
|
|
431
|
+
setattr(self, list_name, el_list)
|
|
432
|
+
return el_list
|
|
433
|
+
|
|
434
|
+
def get_element(self, name: str) -> "Element":
|
|
435
|
+
if self._elements is None or name not in self._elements:
|
|
436
|
+
raise AttributeError(
|
|
437
|
+
f"Элемент '{name}' не найден в контейнере '{self.name}'"
|
|
438
|
+
)
|
|
439
|
+
el = self._elements[name]
|
|
440
|
+
if isinstance(el, ElementList):
|
|
441
|
+
raise AttributeError(
|
|
442
|
+
f"'{name}' является коллекцией. Используйте индекс или вызов."
|
|
443
|
+
)
|
|
444
|
+
return el
|
|
445
|
+
|
|
446
|
+
def _find_element_by_name_recursive(self, name: str) -> Optional["Element"]:
|
|
447
|
+
"""Рекурсивный поиск элемента по имени (Smart Shortcut)."""
|
|
448
|
+
if not self._elements:
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
for el in self._elements.values():
|
|
452
|
+
if isinstance(el, Element):
|
|
453
|
+
if el.name == name:
|
|
454
|
+
return el
|
|
455
|
+
# Recurse
|
|
456
|
+
found = el._find_element_by_name_recursive(name)
|
|
457
|
+
# FIX: Explicit check for None because Element might be Falsy if empty (calls __len__)
|
|
458
|
+
if found is not None:
|
|
459
|
+
return found
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
def __getattr__(self, name: str) -> "Element":
|
|
463
|
+
if name.startswith("_"):
|
|
464
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
|
|
465
|
+
|
|
466
|
+
# 1. Try direct child
|
|
467
|
+
try:
|
|
468
|
+
return self.get_element(name)
|
|
469
|
+
except AttributeError:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
# 2. Check cache
|
|
473
|
+
if name in self._shortcut_cache:
|
|
474
|
+
return self._shortcut_cache[name]
|
|
475
|
+
|
|
476
|
+
# 3. Recursive search (Smart Shortcut)
|
|
477
|
+
found = self._find_element_by_name_recursive(name)
|
|
478
|
+
if found is not None:
|
|
479
|
+
self._shortcut_cache[name] = found
|
|
480
|
+
return found
|
|
481
|
+
|
|
482
|
+
raise AttributeError(
|
|
483
|
+
f"Element '{name}' not found in '{self.name}' or its descendants."
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
def _children_in_order(self) -> List["Element"]:
|
|
487
|
+
if not self._elements:
|
|
488
|
+
return []
|
|
489
|
+
items: List[Element] = []
|
|
490
|
+
for v in self._elements.values():
|
|
491
|
+
if isinstance(v, Element):
|
|
492
|
+
items.append(v)
|
|
493
|
+
return items
|
|
494
|
+
|
|
495
|
+
def __len__(self) -> int:
|
|
496
|
+
return len(self._children_in_order())
|
|
497
|
+
|
|
498
|
+
def __iter__(self) -> Iterator["Element"]:
|
|
499
|
+
yield from self._children_in_order()
|
|
500
|
+
|
|
501
|
+
def __getitem__(self, key: int | str) -> "Element":
|
|
502
|
+
if isinstance(key, int):
|
|
503
|
+
children = self._children_in_order()
|
|
504
|
+
try:
|
|
505
|
+
return children[key]
|
|
506
|
+
except IndexError:
|
|
507
|
+
raise IndexError(f"Index {key} out of range")
|
|
508
|
+
if isinstance(key, str):
|
|
509
|
+
# Support dict-like access via shortcuts too?
|
|
510
|
+
# Usually __getitem__ string access is direct.
|
|
511
|
+
# But let's use getattr logic to be consistent with shortcuts.
|
|
512
|
+
return getattr(self, key)
|
|
513
|
+
raise TypeError("Index must be int or str")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class ElementList:
|
|
517
|
+
"""Коллекция однотипных элементов."""
|
|
518
|
+
|
|
519
|
+
def __init__(self, owner: Element, prototype: Element, name: str) -> None:
|
|
520
|
+
self._owner = owner
|
|
521
|
+
self._prototype = prototype
|
|
522
|
+
self.name = name
|
|
523
|
+
self._prototype.parent = owner
|
|
524
|
+
|
|
525
|
+
def __call__(self, text: str | None = None, **kwargs: Any) -> Element:
|
|
526
|
+
"""
|
|
527
|
+
Возвращает ЭЛЕМЕНТ (не список), найденный по тексту.
|
|
528
|
+
"""
|
|
529
|
+
item = self._make_item(None) # index=None -> ищем среди всех
|
|
530
|
+
|
|
531
|
+
if text:
|
|
532
|
+
item = item.filter(has_text=text)
|
|
533
|
+
|
|
534
|
+
return item
|
|
535
|
+
|
|
536
|
+
def _make_item(self, index: int | None) -> Element:
|
|
537
|
+
cls: Type[Element] = type(self._prototype)
|
|
538
|
+
|
|
539
|
+
# Get field definitions to check init=False
|
|
540
|
+
from dataclasses import fields
|
|
541
|
+
|
|
542
|
+
cls_fields = {f.name: f for f in fields(cls)}
|
|
543
|
+
|
|
544
|
+
kwargs = {
|
|
545
|
+
"name": (
|
|
546
|
+
f"{self._prototype.name}_{index}"
|
|
547
|
+
if index is not None
|
|
548
|
+
else self._prototype.name
|
|
549
|
+
),
|
|
550
|
+
"accessible_name": self._prototype.accessible_name,
|
|
551
|
+
"locator": self._prototype.locator,
|
|
552
|
+
"index": index,
|
|
553
|
+
"exact": self._prototype.exact,
|
|
554
|
+
"description": self._prototype.description,
|
|
555
|
+
"static_screenshot_path": self._prototype.static_screenshot_path,
|
|
556
|
+
"static_aria_snapshot_path": self._prototype.static_aria_snapshot_path,
|
|
557
|
+
"aria_ref": self._prototype.aria_ref,
|
|
558
|
+
"test_id": self._prototype.test_id,
|
|
559
|
+
"placeholder": self._prototype.placeholder,
|
|
560
|
+
"text": self._prototype.text,
|
|
561
|
+
"role": self._prototype.role,
|
|
562
|
+
"url": self._prototype.url,
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
# Filter out fields that have init=False in the target class
|
|
566
|
+
filtered_kwargs = {
|
|
567
|
+
k: v for k, v in kwargs.items() if k not in cls_fields or cls_fields[k].init
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
item = cls(**filtered_kwargs) # type: ignore[arg-type]
|
|
571
|
+
item.parent = self._owner
|
|
572
|
+
if self._prototype._strategies:
|
|
573
|
+
item._strategies = list(self._prototype._strategies)
|
|
574
|
+
|
|
575
|
+
# FIX: Copy filters from prototype!
|
|
576
|
+
if hasattr(self._prototype, "_filters") and self._prototype._filters:
|
|
577
|
+
item._filters = list(self._prototype._filters)
|
|
578
|
+
|
|
579
|
+
return item
|
|
580
|
+
|
|
581
|
+
def __getitem__(self, index: int) -> Element:
|
|
582
|
+
if not isinstance(index, int):
|
|
583
|
+
raise IndexError("Index must be int")
|
|
584
|
+
return self._make_item(index)
|
|
585
|
+
|
|
586
|
+
def get(self, index: int) -> Element:
|
|
587
|
+
return self.__getitem__(index)
|
|
588
|
+
|
|
589
|
+
def first(self) -> Element:
|
|
590
|
+
return self._make_item(0)
|
|
591
|
+
|
|
592
|
+
def last(self) -> Element:
|
|
593
|
+
return self._make_item(-1)
|
|
594
|
+
|
|
595
|
+
def nth(self, index: int) -> Element:
|
|
596
|
+
return self.__getitem__(index)
|
|
597
|
+
|
|
598
|
+
def with_text(self, text: str, *, exact: bool = True) -> Element:
|
|
599
|
+
item = self._make_item(None)
|
|
600
|
+
return item.filter(has_text=text)
|
|
601
|
+
|
|
602
|
+
def filter(
|
|
603
|
+
self,
|
|
604
|
+
has_text: Optional[str | Any] = None,
|
|
605
|
+
has_not_text: Optional[str | Any] = None,
|
|
606
|
+
has: Optional["Element" | Any] = None,
|
|
607
|
+
has_not: Optional["Element" | Any] = None,
|
|
608
|
+
) -> ElementList:
|
|
609
|
+
new_prototype = self._prototype.filter(
|
|
610
|
+
has_text=has_text, has_not_text=has_not_text, has=has, has_not=has_not
|
|
611
|
+
)
|
|
612
|
+
return ElementList(owner=self._owner, prototype=new_prototype, name=self.name)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# --- Примитивные элементы ---
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@dataclass
|
|
619
|
+
class Button(Element):
|
|
620
|
+
role: str = field(default="button", init=False)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@dataclass
|
|
624
|
+
class TextField(Element):
|
|
625
|
+
role: str = field(default="textbox", init=False)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@dataclass
|
|
629
|
+
class Link(Element):
|
|
630
|
+
role: str = field(default="link", init=False)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@dataclass
|
|
634
|
+
class Checkbox(Element):
|
|
635
|
+
role: str = field(default="checkbox", init=False)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
@dataclass
|
|
639
|
+
class Radio(Element):
|
|
640
|
+
role: str = field(default="radio", init=False)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@dataclass
|
|
644
|
+
class Heading(Element):
|
|
645
|
+
role: str = field(default="heading", init=False)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@dataclass
|
|
649
|
+
class Paragraph(Element):
|
|
650
|
+
role: str = field(default="paragraph", init=False)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@dataclass
|
|
654
|
+
class Strong(Element):
|
|
655
|
+
role: str = field(default="strong", init=False)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@dataclass
|
|
659
|
+
class ListElement(Element):
|
|
660
|
+
role: str = field(default="list", init=False)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@dataclass
|
|
664
|
+
class ListItem(Element):
|
|
665
|
+
role: str = field(default="listitem", init=False)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
@dataclass
|
|
669
|
+
class Table(Element):
|
|
670
|
+
role: str = field(default="table", init=False)
|
|
671
|
+
|
|
672
|
+
def rows(self) -> ElementList:
|
|
673
|
+
if hasattr(self, "rows") and isinstance(getattr(self, "rows"), ElementList):
|
|
674
|
+
return getattr(self, "rows")
|
|
675
|
+
return self.add_element_list(
|
|
676
|
+
TableRow(name="row", accessible_name=""), alias="rows", overwrite=True
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def headers(self) -> ElementList:
|
|
680
|
+
if hasattr(self, "headers") and isinstance(
|
|
681
|
+
getattr(self, "headers"), ElementList
|
|
682
|
+
):
|
|
683
|
+
return getattr(self, "headers")
|
|
684
|
+
return self.add_element_list(
|
|
685
|
+
ColumnHeader(name="header", accessible_name=""),
|
|
686
|
+
alias="headers",
|
|
687
|
+
overwrite=True,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def row(
|
|
691
|
+
self, index: int | None = None, *, where: dict[str, str] | None = None
|
|
692
|
+
) -> "TableRow":
|
|
693
|
+
if index is not None and where is None:
|
|
694
|
+
item = TableRow(name=f"row_{index}", accessible_name="", index=index)
|
|
695
|
+
item.parent = self
|
|
696
|
+
return item
|
|
697
|
+
if where is not None and index is None:
|
|
698
|
+
item = TableRow(name="row_where", accessible_name="")
|
|
699
|
+
setattr(item, "_where_criteria", where)
|
|
700
|
+
item.parent = self
|
|
701
|
+
return item
|
|
702
|
+
raise ValueError("Table.row ожидает либо index, либо where.")
|
|
703
|
+
|
|
704
|
+
def header(self, index: int) -> "ColumnHeader":
|
|
705
|
+
item = ColumnHeader(name=f"header_{index}", accessible_name="", index=index)
|
|
706
|
+
item.parent = self
|
|
707
|
+
return item
|
|
708
|
+
|
|
709
|
+
def cell(self, row_index: int, column_index: int) -> "TableCell":
|
|
710
|
+
row_el = self.row(index=row_index)
|
|
711
|
+
cell_el = TableCell(
|
|
712
|
+
name=f"cell_{row_index}_{column_index}",
|
|
713
|
+
accessible_name="",
|
|
714
|
+
index=column_index,
|
|
715
|
+
)
|
|
716
|
+
cell_el.parent = row_el
|
|
717
|
+
return cell_el
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@dataclass
|
|
721
|
+
class ComboBox(Element):
|
|
722
|
+
role: str = field(default="combobox", init=False)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
@dataclass
|
|
726
|
+
class ListBox(Element):
|
|
727
|
+
role: str = field(default="listbox", init=False)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@dataclass
|
|
731
|
+
class Slider(Element):
|
|
732
|
+
role: str = field(default="slider", init=False)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
@dataclass
|
|
736
|
+
class ProgressBar(Element):
|
|
737
|
+
role: str = field(default="progressbar", init=False)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@dataclass
|
|
741
|
+
class SearchBox(Element):
|
|
742
|
+
role: str = field(default="searchbox", init=False)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
@dataclass
|
|
746
|
+
class SpinButton(Element):
|
|
747
|
+
role: str = field(default="spinbutton", init=False)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
@dataclass
|
|
751
|
+
class Switch(Element):
|
|
752
|
+
role: str = field(default="switch", init=False)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@dataclass
|
|
756
|
+
class Code(Element):
|
|
757
|
+
role: str = field(default="code", init=False)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
@dataclass
|
|
761
|
+
class BlockQuote(Element):
|
|
762
|
+
role: str = field(default="blockquote", init=False)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
@dataclass
|
|
766
|
+
class TableRow(Element):
|
|
767
|
+
role: str = field(default="row", init=False)
|
|
768
|
+
_where_criteria: dict[str, str] | None = field(default=None, repr=False, init=False)
|
|
769
|
+
|
|
770
|
+
def _get_playwright_locator(self, base: Any) -> Any:
|
|
771
|
+
# Если это навигационный элемент, используем базовую логику
|
|
772
|
+
if self._nav_type:
|
|
773
|
+
return super()._get_playwright_locator(base)
|
|
774
|
+
|
|
775
|
+
locator = super()._get_playwright_locator(base)
|
|
776
|
+
if not self._where_criteria:
|
|
777
|
+
return locator
|
|
778
|
+
|
|
779
|
+
header_locs = base.locator("th, [role=columnheader]").all()
|
|
780
|
+
headers = [(h.text_content() or "").strip() for h in header_locs]
|
|
781
|
+
if not headers:
|
|
782
|
+
raise ValueError(
|
|
783
|
+
"TableRow: невозможно отфильтровать по where — нет заголовков."
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
filtered = locator
|
|
787
|
+
cell_sel = "td, [role=cell], th, [role=rowheader]"
|
|
788
|
+
for col_name, value in (self._where_criteria or {}).items():
|
|
789
|
+
if col_name not in headers:
|
|
790
|
+
raise ValueError(f"TableRow: не найден заголовок '{col_name}'.")
|
|
791
|
+
col_index = headers.index(col_name)
|
|
792
|
+
cell_in_row = (
|
|
793
|
+
filtered.locator(cell_sel).nth(col_index).filter(has_text=str(value))
|
|
794
|
+
)
|
|
795
|
+
filtered = filtered.filter(has=cell_in_row)
|
|
796
|
+
return filtered
|
|
797
|
+
|
|
798
|
+
def cell(self, column_index: int) -> "TableCell":
|
|
799
|
+
item = TableCell(
|
|
800
|
+
name=f"cell_{column_index}", accessible_name="", index=column_index
|
|
801
|
+
)
|
|
802
|
+
item.parent = self
|
|
803
|
+
return item
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@dataclass
|
|
807
|
+
class TableCell(Element):
|
|
808
|
+
role: str = field(default="cell", init=False)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
@dataclass
|
|
812
|
+
class ColumnHeader(Element):
|
|
813
|
+
role: str = field(default="columnheader", init=False)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@dataclass
|
|
817
|
+
class RowHeader(Element):
|
|
818
|
+
role: str = field(default="rowheader", init=False)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
@dataclass
|
|
822
|
+
class Grid(Element):
|
|
823
|
+
role: str = field(default="grid", init=False)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@dataclass
|
|
827
|
+
class GridCell(Element):
|
|
828
|
+
role: str = field(default="gridcell", init=False)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@dataclass
|
|
832
|
+
class Navigation(Element):
|
|
833
|
+
role: str = field(default="navigation", init=False)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
@dataclass
|
|
837
|
+
class Main(Element):
|
|
838
|
+
role: str = field(default="main", init=False)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
@dataclass
|
|
842
|
+
class Banner(Element):
|
|
843
|
+
role: str = field(default="banner", init=False)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
@dataclass
|
|
847
|
+
class ContentInfo(Element):
|
|
848
|
+
role: str = field(default="contentinfo", init=False)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
@dataclass
|
|
852
|
+
class Region(Element):
|
|
853
|
+
role: str = field(default="region", init=False)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
@dataclass
|
|
857
|
+
class Search(Element):
|
|
858
|
+
role: str = field(default="search", init=False)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
@dataclass
|
|
862
|
+
class Complementary(Element):
|
|
863
|
+
role: str = field(default="complementary", init=False)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
@dataclass
|
|
867
|
+
class Article(Element):
|
|
868
|
+
role: str = field(default="article", init=False)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@dataclass
|
|
872
|
+
class Image(Element):
|
|
873
|
+
role: str = field(default="img", init=False)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
@dataclass
|
|
877
|
+
class Figure(Element):
|
|
878
|
+
role: str = field(default="figure", init=False)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
@dataclass
|
|
882
|
+
class Form(Element):
|
|
883
|
+
role: str = field(default="form", init=False)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@dataclass
|
|
887
|
+
class Group(Element):
|
|
888
|
+
role: str = field(default="group", init=False)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@dataclass
|
|
892
|
+
class RadioGroup(Element):
|
|
893
|
+
role: str = field(default="radiogroup", init=False)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
@dataclass
|
|
897
|
+
class Dialog(Element):
|
|
898
|
+
role: str = field(default="dialog", init=False)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
@dataclass
|
|
902
|
+
class AlertDialog(Element):
|
|
903
|
+
role: str = field(default="alertdialog", init=False)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
@dataclass
|
|
907
|
+
class Alert(Element):
|
|
908
|
+
role: str = field(default="alert", init=False)
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
@dataclass
|
|
912
|
+
class Status(Element):
|
|
913
|
+
role: str = field(default="status", init=False)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@dataclass
|
|
917
|
+
class Log(Element):
|
|
918
|
+
role: str = field(default="log", init=False)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
@dataclass
|
|
922
|
+
class Timer(Element):
|
|
923
|
+
role: str = field(default="timer", init=False)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
@dataclass
|
|
927
|
+
class Menu(Element):
|
|
928
|
+
role: str = field(default="menu", init=False)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
@dataclass
|
|
932
|
+
class MenuBar(Element):
|
|
933
|
+
role: str = field(default="menubar", init=False)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
@dataclass
|
|
937
|
+
class MenuItem(Element):
|
|
938
|
+
role: str = field(default="menuitem", init=False)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
@dataclass
|
|
942
|
+
class MenuItemCheckbox(Element):
|
|
943
|
+
role: str = field(default="menuitemcheckbox", init=False)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
@dataclass
|
|
947
|
+
class MenuItemRadio(Element):
|
|
948
|
+
role: str = field(default="menuitemradio", init=False)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
@dataclass
|
|
952
|
+
class TabList(Element):
|
|
953
|
+
role: str = field(default="tablist", init=False)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
@dataclass
|
|
957
|
+
class Tab(Element):
|
|
958
|
+
role: str = field(default="tab", init=False)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
@dataclass
|
|
962
|
+
class TabPanel(Element):
|
|
963
|
+
role: str = field(default="tabpanel", init=False)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
@dataclass
|
|
967
|
+
class Tree(Element):
|
|
968
|
+
role: str = field(default="tree", init=False)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
@dataclass
|
|
972
|
+
class TreeItem(Element):
|
|
973
|
+
role: str = field(default="treeitem", init=False)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@dataclass
|
|
977
|
+
class Meter(Element):
|
|
978
|
+
role: str = field(default="meter", init=False)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
@dataclass
|
|
982
|
+
class ScrollBar(Element):
|
|
983
|
+
role: str = field(default="scrollbar", init=False)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
@dataclass
|
|
987
|
+
class Separator(Element):
|
|
988
|
+
role: str = field(default="separator", init=False)
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@dataclass
|
|
992
|
+
class Toolbar(Element):
|
|
993
|
+
role: str = field(default="toolbar", init=False)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
@dataclass
|
|
997
|
+
class Tooltip(Element):
|
|
998
|
+
role: str = field(default="tooltip", init=False)
|