persona-dsl 26.1.20.8__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.
Files changed (86) hide show
  1. persona_dsl/__init__.py +35 -0
  2. persona_dsl/components/action.py +10 -0
  3. persona_dsl/components/base_step.py +251 -0
  4. persona_dsl/components/combined_step.py +68 -0
  5. persona_dsl/components/expectation.py +10 -0
  6. persona_dsl/components/fact.py +10 -0
  7. persona_dsl/components/goal.py +10 -0
  8. persona_dsl/components/ops.py +7 -0
  9. persona_dsl/components/step.py +75 -0
  10. persona_dsl/expectations/generic/__init__.py +15 -0
  11. persona_dsl/expectations/generic/contains_item.py +19 -0
  12. persona_dsl/expectations/generic/contains_the_text.py +15 -0
  13. persona_dsl/expectations/generic/has_entries.py +21 -0
  14. persona_dsl/expectations/generic/is_equal.py +24 -0
  15. persona_dsl/expectations/generic/is_greater_than.py +18 -0
  16. persona_dsl/expectations/generic/path_equal.py +27 -0
  17. persona_dsl/expectations/web/__init__.py +5 -0
  18. persona_dsl/expectations/web/is_displayed.py +13 -0
  19. persona_dsl/expectations/web/matches_aria_snapshot.py +221 -0
  20. persona_dsl/expectations/web/matches_screenshot.py +160 -0
  21. persona_dsl/generators/__init__.py +5 -0
  22. persona_dsl/generators/api_generator.py +423 -0
  23. persona_dsl/generators/cli.py +431 -0
  24. persona_dsl/generators/page_generator.py +1140 -0
  25. persona_dsl/ops/api/__init__.py +5 -0
  26. persona_dsl/ops/api/json_as.py +104 -0
  27. persona_dsl/ops/api/json_response.py +48 -0
  28. persona_dsl/ops/api/send_request.py +41 -0
  29. persona_dsl/ops/db/__init__.py +5 -0
  30. persona_dsl/ops/db/execute_sql.py +22 -0
  31. persona_dsl/ops/db/fetch_all.py +29 -0
  32. persona_dsl/ops/db/fetch_one.py +22 -0
  33. persona_dsl/ops/kafka/__init__.py +4 -0
  34. persona_dsl/ops/kafka/message_in_topic.py +89 -0
  35. persona_dsl/ops/kafka/send_message.py +35 -0
  36. persona_dsl/ops/soap/__init__.py +4 -0
  37. persona_dsl/ops/soap/call_operation.py +24 -0
  38. persona_dsl/ops/soap/operation_result.py +24 -0
  39. persona_dsl/ops/web/__init__.py +37 -0
  40. persona_dsl/ops/web/aria_snapshot.py +87 -0
  41. persona_dsl/ops/web/click.py +30 -0
  42. persona_dsl/ops/web/current_path.py +17 -0
  43. persona_dsl/ops/web/element_attribute.py +24 -0
  44. persona_dsl/ops/web/element_is_visible.py +27 -0
  45. persona_dsl/ops/web/element_text.py +28 -0
  46. persona_dsl/ops/web/elements_count.py +42 -0
  47. persona_dsl/ops/web/fill.py +41 -0
  48. persona_dsl/ops/web/generate_page_object.py +118 -0
  49. persona_dsl/ops/web/input_value.py +23 -0
  50. persona_dsl/ops/web/navigate.py +52 -0
  51. persona_dsl/ops/web/press_key.py +37 -0
  52. persona_dsl/ops/web/rich_aria_snapshot.py +146 -0
  53. persona_dsl/ops/web/screenshot.py +68 -0
  54. persona_dsl/ops/web/table_data.py +43 -0
  55. persona_dsl/ops/web/wait_for_navigation.py +23 -0
  56. persona_dsl/pages/__init__.py +133 -0
  57. persona_dsl/pages/elements.py +998 -0
  58. persona_dsl/pages/page.py +44 -0
  59. persona_dsl/pages/virtual_page.py +94 -0
  60. persona_dsl/persona.py +125 -0
  61. persona_dsl/pytest_plugin.py +1064 -0
  62. persona_dsl/runtime/dist/persona_bundle.js +1077 -0
  63. persona_dsl/skills/__init__.py +7 -0
  64. persona_dsl/skills/core/base.py +41 -0
  65. persona_dsl/skills/core/skill_definition.py +30 -0
  66. persona_dsl/skills/use_api.py +251 -0
  67. persona_dsl/skills/use_browser.py +78 -0
  68. persona_dsl/skills/use_database.py +129 -0
  69. persona_dsl/skills/use_kafka.py +135 -0
  70. persona_dsl/skills/use_soap.py +66 -0
  71. persona_dsl/utils/__init__.py +0 -0
  72. persona_dsl/utils/artifacts.py +22 -0
  73. persona_dsl/utils/config.py +54 -0
  74. persona_dsl/utils/data_providers.py +159 -0
  75. persona_dsl/utils/decorators.py +80 -0
  76. persona_dsl/utils/metrics.py +69 -0
  77. persona_dsl/utils/naming.py +14 -0
  78. persona_dsl/utils/path.py +202 -0
  79. persona_dsl/utils/retry.py +51 -0
  80. persona_dsl/utils/taas_integration.py +124 -0
  81. persona_dsl/utils/waits.py +112 -0
  82. persona_dsl-26.1.20.8.dist-info/METADATA +35 -0
  83. persona_dsl-26.1.20.8.dist-info/RECORD +86 -0
  84. persona_dsl-26.1.20.8.dist-info/WHEEL +5 -0
  85. persona_dsl-26.1.20.8.dist-info/entry_points.txt +6 -0
  86. persona_dsl-26.1.20.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,35 @@
1
+ from .components.action import Action
2
+ from .utils.decorators import data_driven, tag, parametrize_simple
3
+ from .persona import BaseRole
4
+ from .skills.core.skill_definition import BaseSkill
5
+ from .components.expectation import Expectation
6
+ from .components.fact import Fact
7
+ from .components.goal import Goal
8
+ from .persona import Persona
9
+ from .skills.core.skill_definition import SkillId
10
+ from .utils.waits import wait_until
11
+ from .skills.core.base import Skill
12
+ from .skills import UseAPI, UseBrowser, UseDatabase, UseKafka, UseSOAP
13
+ from .components.step import Step
14
+
15
+ __all__ = [
16
+ "Action",
17
+ "Expectation",
18
+ "Goal",
19
+ "Persona",
20
+ "Fact",
21
+ "SkillId",
22
+ "Step",
23
+ "data_driven",
24
+ "tag",
25
+ "parametrize_simple",
26
+ "BaseRole",
27
+ "BaseSkill",
28
+ "Skill",
29
+ "wait_until",
30
+ "UseAPI",
31
+ "UseBrowser",
32
+ "UseDatabase",
33
+ "UseKafka",
34
+ "UseSOAP",
35
+ ]
@@ -0,0 +1,10 @@
1
+ from .step import Step
2
+
3
+
4
+ class Action(Step):
5
+ """
6
+ Алиас для Step, предназначенный для story-стиля.
7
+ Не содержит собственной логики.
8
+ """
9
+
10
+ pass
@@ -0,0 +1,251 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Optional, TypeVar
7
+
8
+ from allure import step
9
+
10
+ from ..utils.artifacts import artifacts_dir
11
+ from ..utils.metrics import metrics
12
+ from ..utils.path import extract_by_path
13
+ from ..utils.taas_integration import publish_taas_event
14
+ from ..utils.retry import RetryPolicy
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ T = TypeVar("T", bound="BaseStep")
19
+
20
+
21
+ class BaseStep(ABC):
22
+ """Единый базовый класс для всех исполняемых компонентов."""
23
+
24
+ # Политика ретраев по умолчанию для класса (может быть переопределена в наследниках)
25
+ retry_policy: Optional[RetryPolicy] = None
26
+
27
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
28
+ self._instance_retry_policy: Optional[RetryPolicy] = None
29
+
30
+ def __repr__(self) -> str:
31
+ params = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
32
+ param_str = ", ".join(f"{k}={v!r}" for k, v in params.items())
33
+ return f"{self.__class__.__name__}({param_str})"
34
+
35
+ def with_retry(self: T, policy: RetryPolicy) -> T:
36
+ """
37
+ Возвращает копию шага с установленной политикой ретраев.
38
+ Полезно для ad-hoc настройки надежности в конкретном сценарии.
39
+ """
40
+ import copy
41
+
42
+ new_step = copy.copy(self)
43
+ new_step._instance_retry_policy = policy
44
+ return new_step
45
+
46
+ def _get_effective_retry_policy(self, persona: Any) -> RetryPolicy:
47
+ """Определяет актуальную политику ретраев (Instance > Class > Global Default)."""
48
+ # 1. Instance level (через .with_retry())
49
+ instance_policy = getattr(self, "_instance_retry_policy", None)
50
+ if instance_policy:
51
+ return instance_policy
52
+
53
+ # 2. Class level (атрибут класса)
54
+ if self.retry_policy:
55
+ return self.retry_policy
56
+
57
+ # 3. Global default from Persona config
58
+ # Ожидаем, что в persona.retries_config лежит dict, например:
59
+ # {'default': {...}, 'web_actions': {...}}
60
+ if hasattr(persona, "retries_config") and persona.retries_config:
61
+ # Можно усложнить логику и искать по типу шага (web_actions и т.д.)
62
+ # Пока берем 'default'
63
+ default_cfg = persona.retries_config.get("default")
64
+ if default_cfg:
65
+ return RetryPolicy(**default_cfg)
66
+
67
+ # 4. Hard fallback
68
+ return RetryPolicy(max_attempts=1)
69
+
70
+ def _get_component_type(self) -> str:
71
+ """
72
+ Определяет тип компонента на основе иерархии классов.
73
+ """
74
+ # Локальные импорты для разрыва циклической зависимости
75
+ from .goal import Goal
76
+ from .fact import Fact
77
+ from .action import Action
78
+ from .expectation import Expectation
79
+ from .combined_step import CombinedStep
80
+ from .step import Step
81
+ from .ops import Ops
82
+
83
+ if isinstance(self, Goal):
84
+ return "goal"
85
+ if isinstance(self, Fact):
86
+ return "fact"
87
+ if isinstance(self, Action):
88
+ return "action"
89
+ if isinstance(self, Expectation):
90
+ return "check"
91
+ if isinstance(self, CombinedStep):
92
+ return "combined_step"
93
+ if isinstance(self, Step):
94
+ return "step"
95
+ if isinstance(self, Ops):
96
+ if hasattr(self, "_component_type"):
97
+ comp_type = getattr(self, "_component_type")
98
+ if isinstance(comp_type, str):
99
+ return comp_type
100
+ return "ops"
101
+ return "unknown"
102
+
103
+ def execute(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
104
+ """
105
+ Универсальная обертка для выполнения шага с поддержкой ретраев.
106
+ """
107
+ description = self._get_step_description(persona)
108
+ start_time = time.time()
109
+ component_type = self._get_component_type()
110
+ policy = self._get_effective_retry_policy(persona)
111
+
112
+ params = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
113
+ publish_taas_event(
114
+ {
115
+ "event": "step_start",
116
+ "type": component_type,
117
+ "data": {
118
+ "description": description,
119
+ "timestamp": start_time,
120
+ "component_name": self.__class__.__name__,
121
+ "component_params": params,
122
+ "max_attempts": policy.max_attempts,
123
+ },
124
+ }
125
+ )
126
+
127
+ value: Any = None
128
+ status = "passed"
129
+ attempt = 0
130
+ attempt = 0
131
+
132
+ try:
133
+ # Цикл ретраев
134
+ while True:
135
+ attempt += 1
136
+ try:
137
+ with step(
138
+ description
139
+ if attempt == 1
140
+ else f"{description} (попытка {attempt})"
141
+ ):
142
+ if attempt > 1 and policy.on_retry_action:
143
+ step("Предварительное действие перед повтором")
144
+ policy.execute_action()
145
+
146
+ value = self._perform(persona, *args, **kwargs)
147
+ break # Успех
148
+ except Exception as e:
149
+ if policy.should_retry(attempt, e):
150
+ logger.warning(
151
+ f"Шаг '{description}' упал (попытка {attempt}/{policy.max_attempts}): {e}. Ретрай через {policy.delay}с."
152
+ )
153
+ policy.wait(attempt)
154
+ else:
155
+ raise e
156
+
157
+ # Привязка результата
158
+ save_as = getattr(self, "save_as", None)
159
+ extract = getattr(self, "extract", None)
160
+ if save_as:
161
+ bound_value = (
162
+ extract_by_path(value, extract)
163
+ if isinstance(extract, str)
164
+ else value
165
+ )
166
+ persona.memory[save_as] = bound_value
167
+
168
+ if value is not None:
169
+ with step(f"Получено значение: {str(value)[:100]}"):
170
+ pass
171
+
172
+ return value
173
+
174
+ except Exception:
175
+ status = "failed"
176
+ raise
177
+ finally:
178
+ end_time = time.time()
179
+ duration = end_time - start_time
180
+ metrics.gauge(
181
+ "step.duration",
182
+ duration,
183
+ tags={
184
+ "type": component_type,
185
+ "name": self.__class__.__name__,
186
+ "status": status,
187
+ },
188
+ )
189
+
190
+ event_data_end: dict[str, Any] = {
191
+ "description": description,
192
+ "timestamp": end_time,
193
+ "duration": duration,
194
+ "status": status,
195
+ "attempts_used": attempt,
196
+ }
197
+
198
+ result_raw: Any = None
199
+ result_raw_path: str | None = None
200
+ result_raw_url: str | None = None
201
+ if isinstance(value, (dict, list)):
202
+ result_raw = value
203
+ raw_dir = artifacts_dir("result_raw")
204
+ filename = f"{int(end_time * 1000)}-{self.__class__.__name__}.json"
205
+ file_path = raw_dir / filename
206
+ try:
207
+ with open(file_path, "w", encoding="utf-8") as f:
208
+ json.dump(result_raw, f, ensure_ascii=False, indent=2)
209
+ result_raw_path = str(file_path)
210
+ run_id = os.environ.get("TAAS_RUN_ID")
211
+ result_raw_url = (
212
+ f"/artifacts/{run_id}/result_raw/{filename}" if run_id else None
213
+ )
214
+ except (IOError, TypeError) as e:
215
+ logger.warning(
216
+ f"Не удалось сохранить result_raw артефакт в {file_path}: {e}"
217
+ )
218
+ result_raw = None
219
+
220
+ event_data_end.update(
221
+ {
222
+ "result": str(value)[:100] if value is not None else "None",
223
+ "result_raw": result_raw,
224
+ "bindings": {
225
+ "save_as": getattr(self, "save_as", None),
226
+ "extract": getattr(self, "extract", None),
227
+ },
228
+ "result_raw_path": result_raw_path,
229
+ "result_raw_url": result_raw_url,
230
+ }
231
+ )
232
+
233
+ publish_taas_event(
234
+ {
235
+ "event": "step_end",
236
+ "type": component_type,
237
+ "data": event_data_end,
238
+ }
239
+ )
240
+
241
+ @abstractmethod
242
+ def _get_step_description(self, persona: Any) -> str: # pragma: no cover
243
+ """Возвращает текстовое описание шага для Allure."""
244
+ ...
245
+
246
+ @abstractmethod
247
+ def _perform(
248
+ self, persona: Any, *args: Any, **kwargs: Any
249
+ ) -> Any: # pragma: no cover
250
+ """Содержит реальную логику выполнения шага."""
251
+ ...
@@ -0,0 +1,68 @@
1
+ from abc import abstractmethod
2
+ from typing import Any, Generator
3
+
4
+ from .base_step import BaseStep
5
+
6
+
7
+ class CombinedStep(BaseStep):
8
+ """
9
+ Базовый класс для высокоуровневого, композитного шага (Combined Step),
10
+ который объединяет последовательность других шагов (Steps) для достижения
11
+ бизнес-цели. Управляется через генератор.
12
+
13
+ Также предоставляет метод make(persona) для story-стиля, который
14
+ просто вызывает execute(persona) без добавления логики.
15
+
16
+ Основной контракт — реализация генераторного метода _run(), который
17
+ позволяет описывать как простые последовательности Steps, так и сложную
18
+ логику с передачей результатов между ними.
19
+ """
20
+
21
+ def make(self, persona: Any) -> None:
22
+ self.execute(persona)
23
+
24
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
25
+ """
26
+ Исполняет генераторный сценарий из `_run`, передавая результаты
27
+ выполнения Steps обратно в генератор.
28
+ """
29
+ from .step import Step
30
+
31
+ gen = self._run(persona, *args, **kwargs)
32
+ try:
33
+ # Первый вызов для запуска генератора и получения первого шага
34
+ step_to_execute = gen.send(None)
35
+ while True:
36
+ # Проверяем, что из CombinedStep не yield'ят низко-уровневые Ops
37
+ if not isinstance(step_to_execute, (Step, CombinedStep)):
38
+ raise TypeError(
39
+ f"Компонент '{self.__class__.__name__}' (наследник CombinedStep) "
40
+ f"может yield'ить только другие шаги (наследники Step/CombinedStep), "
41
+ f"но был получен объект типа {type(step_to_execute).__name__}."
42
+ )
43
+
44
+ # Выполняем полученный шаг
45
+ last_result = step_to_execute.execute(persona)
46
+ # Отправляем результат обратно в генератор и получаем следующий шаг
47
+ step_to_execute = gen.send(last_result)
48
+ except StopIteration as e:
49
+ # Генератор завершился (оператором return), его результат в e.value
50
+ return e.value
51
+
52
+ @abstractmethod
53
+ def _get_step_description(self, persona: Any) -> str: # pragma: no cover
54
+ """Возвращает текстовое описание шага для Allure."""
55
+ ...
56
+
57
+ @abstractmethod
58
+ def _run(
59
+ self, persona: Any, *args: Any, **kwargs: Any
60
+ ) -> Generator[Any, Any, Any]: # pragma: no cover
61
+ """
62
+ Декларативно-императивный контракт.
63
+ Yield'ит последовательность Steps и явно возвращает результат.
64
+ """
65
+ # Этот код нужен, чтобы сделать метод абстрактным генератором.
66
+ # В реальных реализациях он будет заменён.
67
+ if False:
68
+ yield
@@ -0,0 +1,10 @@
1
+ from .step import Step
2
+
3
+
4
+ class Expectation(Step):
5
+ """
6
+ Алиас для Step, предназначенный для story-стиля.
7
+ Не содержит собственной логики.
8
+ """
9
+
10
+ pass
@@ -0,0 +1,10 @@
1
+ from .step import Step
2
+
3
+
4
+ class Fact(Step):
5
+ """
6
+ Алиас для Step, предназначенный для story-стиля.
7
+ Не содержит собственной логики.
8
+ """
9
+
10
+ pass
@@ -0,0 +1,10 @@
1
+ from .combined_step import CombinedStep
2
+
3
+
4
+ class Goal(CombinedStep):
5
+ """
6
+ Алиас для CombinedStep, предназначенный для story-стиля.
7
+ Не содержит собственной логики.
8
+ """
9
+
10
+ pass
@@ -0,0 +1,7 @@
1
+ from .base_step import BaseStep
2
+
3
+
4
+ class Ops(BaseStep):
5
+ """Базовый класс для всех низкоуровневых, исполняемых операций."""
6
+
7
+ pass
@@ -0,0 +1,75 @@
1
+ from typing import Any, Generator
2
+
3
+ from .base_step import BaseStep
4
+
5
+
6
+ class Step(BaseStep):
7
+ """
8
+ Базовый класс для доменно-ориентированных шагов (Steps),
9
+ которые инкапсулируют бизнес-логику и состоят из последовательности
10
+ низкоуровневых операций (Ops), управляемых через генератор.
11
+
12
+ Предоставляет универсальные методы-обёртки для разных стилей написания:
13
+ - perform_as(persona): story-стиль для действий
14
+ - get(persona): story-стиль для фактов (возвращает значение)
15
+ - check(persona, actual_value): story-стиль для проверок
16
+
17
+ Основной контракт — реализация генераторного метода _run(), который
18
+ позволяет описывать как простые последовательности Ops, так и сложную
19
+ логику с передачей результатов между операциями.
20
+ """
21
+
22
+ def perform_as(self, persona: Any) -> None:
23
+ self.execute(persona)
24
+
25
+ def get(self, persona: Any) -> Any:
26
+ return self.execute(persona)
27
+
28
+ def check(self, persona: Any, actual_value: Any) -> None:
29
+ self.execute(persona, actual_value)
30
+
31
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
32
+ """
33
+ Исполняет генераторный сценарий из `_run`, передавая результаты
34
+ выполнения Ops обратно в генератор.
35
+ """
36
+ from .ops import Ops
37
+
38
+ gen = self._run(persona, *args, **kwargs)
39
+ try:
40
+ # Первый вызов для запуска генератора и получения первой операции
41
+ op_to_execute = gen.send(None)
42
+ while True:
43
+ # Проверяем, что из Step yield'ят только Ops
44
+ if not isinstance(op_to_execute, Ops):
45
+ raise TypeError(
46
+ f"Компонент '{self.__class__.__name__}' (наследник Step) "
47
+ f"может yield'ить только объекты-наследники Ops, "
48
+ f"но был получен объект типа {type(op_to_execute).__name__}."
49
+ )
50
+
51
+ # Выполняем полученную операцию
52
+ current_result = op_to_execute.execute(persona)
53
+ # Отправляем результат обратно в генератор и получаем следующую операцию
54
+ op_to_execute = gen.send(current_result)
55
+ except StopIteration as e:
56
+ # Генератор завершился (оператором return), его результат в e.value
57
+ return e.value
58
+
59
+ def _run(self, persona: Any, *args: Any, **kwargs: Any) -> Generator[Any, Any, Any]:
60
+ """
61
+ Декларативно-императивный контракт.
62
+ Yield'ит последовательность Ops и явно возвращает результат.
63
+
64
+ По умолчанию выбрасывает NotImplementedError. Наследники должны либо:
65
+ 1. Реализовать этот метод (генераторный стиль).
66
+ 2. Переопределить _perform (императивный стиль).
67
+ """
68
+ raise NotImplementedError(
69
+ f"Класс {self.__class__.__name__} должен реализовать метод '_run' (генераторный стиль) "
70
+ f"или переопределить метод '_perform' (императивный стиль)."
71
+ )
72
+ # Этот yield нужен, чтобы Python считал метод генератором (даже если он сразу падает)
73
+ # Это важно для статических анализаторов и некоторых проверок.
74
+ if False:
75
+ yield
@@ -0,0 +1,15 @@
1
+ from .is_equal import IsEqualTo
2
+ from .contains_the_text import ContainsTheText
3
+ from .has_entries import HasEntries
4
+ from .contains_item import ContainsItem
5
+ from .is_greater_than import IsGreaterThan
6
+ from .path_equal import PathEqual
7
+
8
+ __all__ = [
9
+ "IsEqualTo",
10
+ "ContainsTheText",
11
+ "HasEntries",
12
+ "ContainsItem",
13
+ "IsGreaterThan",
14
+ "PathEqual",
15
+ ]
@@ -0,0 +1,19 @@
1
+ from typing import Any
2
+
3
+ from persona_dsl.components.expectation import Expectation
4
+ from hamcrest import assert_that, has_item
5
+
6
+
7
+ class ContainsItem(Expectation):
8
+ """Проверяет, что список содержит указанный элемент."""
9
+
10
+ def __init__(self, expected_item: Any):
11
+ self.expected_item = expected_item
12
+
13
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
14
+ """Проверяет, что список содержит указанный элемент."""
15
+ actual_list = args[0]
16
+ assert_that(actual_list, has_item(self.expected_item))
17
+
18
+ def _get_step_description(self, persona: Any) -> str:
19
+ return f"содержит элемент '{self.expected_item}'"
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+ from persona_dsl.components.expectation import Expectation
3
+ from hamcrest import assert_that, contains_string
4
+
5
+
6
+ class ContainsTheText(Expectation):
7
+ def __init__(self, expected_text: str):
8
+ self.expected_text = expected_text
9
+
10
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
11
+ actual_text = args[0]
12
+ assert_that(actual_text, contains_string(self.expected_text))
13
+
14
+ def _get_step_description(self, persona: Any) -> str:
15
+ return f"содержит текст '{self.expected_text}'"
@@ -0,0 +1,21 @@
1
+ from typing import Dict, Any
2
+ from persona_dsl.components.expectation import Expectation
3
+ from hamcrest import assert_that, has_entries
4
+
5
+
6
+ class HasEntries(Expectation):
7
+ """
8
+ Проверяет, что фактический словарь содержит все ключи и значения
9
+ из ожидаемого словаря.
10
+ """
11
+
12
+ def __init__(self, expected_data: Dict[str, Any]):
13
+ self.expected_data = expected_data
14
+
15
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
16
+ """Проверяет, что actual_data содержит все записи из expected_data."""
17
+ actual_data = args[0]
18
+ assert_that(actual_data, has_entries(self.expected_data))
19
+
20
+ def _get_step_description(self, persona: Any) -> str:
21
+ return f"содержит записи {self.expected_data}"
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+
3
+ from persona_dsl.components.expectation import Expectation
4
+ from hamcrest import assert_that, equal_to
5
+
6
+
7
+ class IsEqualTo(Expectation):
8
+ """Проверяет, что фактическое значение равно ожидаемому."""
9
+
10
+ def __init__(self, expected: Any):
11
+ """Инициализирует ожидание.
12
+
13
+ Args:
14
+ expected: Ожидаемое значение.
15
+ """
16
+ self.expected = expected
17
+
18
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
19
+ """Проверяет, что фактическое значение равно ожидаемому."""
20
+ actual = args[0]
21
+ assert_that(actual, equal_to(self.expected))
22
+
23
+ def _get_step_description(self, persona: Any) -> str:
24
+ return f"равно '{self.expected}'"
@@ -0,0 +1,18 @@
1
+ from typing import Any
2
+ from persona_dsl.components.expectation import Expectation
3
+ from hamcrest import assert_that, greater_than
4
+
5
+
6
+ class IsGreaterThan(Expectation):
7
+ """Проверяет, что одно число больше другого."""
8
+
9
+ def __init__(self, value: int | float):
10
+ self.value = value
11
+
12
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
13
+ """Проверяет, что одно число больше другого."""
14
+ actual_value = args[0]
15
+ assert_that(actual_value, greater_than(self.value))
16
+
17
+ def _get_step_description(self, persona: Any) -> str:
18
+ return f"больше чем {self.value}"
@@ -0,0 +1,27 @@
1
+ from typing import Any
2
+
3
+ from hamcrest import assert_that, equal_to
4
+
5
+ from persona_dsl.components.expectation import Expectation
6
+ from persona_dsl.utils.path import extract_by_path
7
+
8
+
9
+ class PathEqual(Expectation):
10
+ """
11
+ Проверяет, что значение по указанному пути (dot/bracket нотация) внутри объекта равно ожидаемому.
12
+
13
+ Пример:
14
+ PathEqual("a.b[0].c", 42).check({"a": {"b": [{"c": 42}]}}) # проходит
15
+ """
16
+
17
+ def __init__(self, path: str, expected: Any):
18
+ self.path = path
19
+ self.expected = expected
20
+
21
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
22
+ actual = args[0]
23
+ value = extract_by_path(actual, self.path)
24
+ assert_that(value, equal_to(self.expected))
25
+
26
+ def _get_step_description(self, persona: Any) -> str:
27
+ return f"значение по пути '{self.path}' равно '{self.expected}'"
@@ -0,0 +1,5 @@
1
+ from .is_displayed import IsDisplayed
2
+ from .matches_screenshot import MatchesScreenshot
3
+ from .matches_aria_snapshot import MatchesAriaSnapshot
4
+
5
+ __all__ = ["IsDisplayed", "MatchesScreenshot", "MatchesAriaSnapshot"]
@@ -0,0 +1,13 @@
1
+ from typing import Any
2
+ from persona_dsl.components.expectation import Expectation
3
+
4
+
5
+ class IsDisplayed(Expectation):
6
+ """Проверяет, что значение (результат факта) равно True."""
7
+
8
+ def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
9
+ actual_value = args[0]
10
+ assert actual_value is True, "Элемент не отображается, хотя ожидалось обратное."
11
+
12
+ def _get_step_description(self, persona: Any) -> str:
13
+ return "отображается на странице"