persona-dsl 26.1.21.30__py3-none-any.whl → 26.1.21.32__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.
@@ -14,6 +14,7 @@ from urllib.parse import urlparse
14
14
  import allure
15
15
  import pytest
16
16
  import yaml
17
+ import requests
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from persona_dsl.components.expectation import Expectation
@@ -116,6 +117,57 @@ def _get_skill_config(
116
117
  return named_config if isinstance(named_config, dict) else None
117
118
 
118
119
 
120
+ def _create_selenoid_session(
121
+ hub_url: str, capabilities: Dict[str, Any], retries: int = 3, timeout: float = 10.0
122
+ ) -> Tuple[str, str]:
123
+ """
124
+ Создает сессию в Selenoid через WebDriver API с повторными попытками.
125
+ Возвращает кортеж (ws_endpoint, session_id).
126
+ """
127
+ session_url = f"{hub_url.rstrip('/')}/wd/hub/session"
128
+ last_error: Optional[Exception] = None
129
+
130
+ for attempt in range(retries + 1):
131
+ try:
132
+ logger.info(
133
+ f"Попытка создания сессии Selenoid ({attempt + 1}/{retries + 1})..."
134
+ )
135
+ # requests imported at module level
136
+ resp = requests.post(
137
+ session_url, json={"capabilities": capabilities}, timeout=timeout
138
+ )
139
+ if resp.status_code == 200:
140
+ data = resp.json()
141
+ # Формат ответа WebDriver: { "value": { "sessionId": "...", "capabilities": ... } }
142
+ value = data.get("value", {})
143
+ session_id = value.get("sessionId") or data.get("sessionId")
144
+
145
+ if not session_id:
146
+ raise RuntimeError(f"Selenoid не вернул sessionId: {data}")
147
+
148
+ # Формируем WS URL для CDP
149
+ parsed = urlparse(hub_url)
150
+ ws_scheme = "wss" if parsed.scheme == "https" else "ws"
151
+ ws_endpoint = f"{ws_scheme}://{parsed.netloc}/devtools/{session_id}"
152
+
153
+ logger.info(f"Сессия Selenoid создана: {session_id}")
154
+ return ws_endpoint, session_id
155
+ else:
156
+ logger.warning(
157
+ f"Ошибка создания сессии Selenoid (Code {resp.status_code}): {resp.text}"
158
+ )
159
+ except Exception as e:
160
+ last_error = e
161
+ logger.warning(f"Ошибка подключения к Selenoid: {e}")
162
+
163
+ if attempt < retries:
164
+ time.sleep(2.0)
165
+
166
+ raise RuntimeError(
167
+ f"Не удалось создать сессию Selenoid после {retries + 1} попыток. Последняя ошибка: {last_error}"
168
+ )
169
+
170
+
119
171
  def _get_persona_config(config: Config, role: str) -> Optional[Dict[str, Any]]:
120
172
  personas_config = config.personas
121
173
  if role == "default":
@@ -131,6 +183,38 @@ def _get_persona_config(config: Config, role: str) -> Optional[Dict[str, Any]]:
131
183
  return named_config if isinstance(named_config, dict) else None
132
184
 
133
185
 
186
+ def _build_chromium_args(cfg: Dict[str, Any]) -> List[str]:
187
+ """Строит список аргументов командной строки для Chromium-based браузеров."""
188
+ args = list(cfg.get("args") or [])
189
+
190
+ if cfg.get("no_sandbox", False):
191
+ args.append("--no-sandbox")
192
+
193
+ disable_features = list(cfg.get("disable_features") or [])
194
+
195
+ if cfg.get("ignore_https_errors", False):
196
+ args.append("--ignore-certificate-errors")
197
+ # Отключаем принудительное обновление до HTTPS и связанные с ним предупреждения
198
+ if "HttpsUpgrades" not in disable_features:
199
+ disable_features.append("HttpsUpgrades")
200
+ if "EnforceHttps" not in disable_features:
201
+ disable_features.append("EnforceHttps")
202
+
203
+ # Для HTTP сайтов отключаем предупреждения о небезопасности
204
+ base_url = cfg.get("base_url")
205
+ if base_url:
206
+ parsed_url = urlparse(base_url)
207
+ if parsed_url.scheme == "http":
208
+ origin = f"{parsed_url.scheme}://{parsed_url.netloc}"
209
+ args.append(f"--unsafely-treat-insecure-origin-as-secure={origin}")
210
+
211
+ if disable_features:
212
+ features_str = ",".join(disable_features)
213
+ args.append(f"--disable-features={features_str}")
214
+
215
+ return args
216
+
217
+
134
218
  class PersonaDispatcher:
135
219
  def __init__(
136
220
  self,
@@ -234,49 +318,116 @@ class PersonaDispatcher:
234
318
  pw = self._resources["playwright"]
235
319
  browser_type = getattr(pw, cfg.get("type", "chromium"))
236
320
 
321
+ selenoid_url = cfg.get("selenoid_url")
237
322
  ws_endpoint = cfg.get("ws_endpoint")
238
323
 
239
- if ws_endpoint:
324
+ if selenoid_url:
325
+ # Создание сессии в Selenoid
326
+ selenoid_opts = cfg.get("selenoid_options", {})
327
+
328
+ # Определяем browserName для Selenoid
329
+ requested_type = cfg.get("type", "chromium")
330
+ if requested_type == "sberbrowser":
331
+ browser_name = "sberbrowser"
332
+ # Playwright не знает про sberbrowser, используем chromium
333
+ browser_type = getattr(pw, "chromium")
334
+ elif requested_type == "chromium":
335
+ browser_name = "chrome" # Selenoid обычно использует "chrome" или "chromium"
336
+ else:
337
+ browser_name = requested_type
338
+
339
+ # Базовые capabilities
340
+ caps = {
341
+ "browserName": browser_name,
342
+ "version": cfg.get("version", ""),
343
+ "enableVNC": selenoid_opts.get("enableVNC", True),
344
+ "enableVideo": selenoid_opts.get("enableVideo", False),
345
+ }
346
+ # Добавляем специфичные опции
347
+ if "goog:chromeOptions" in selenoid_opts:
348
+ caps["goog:chromeOptions"] = selenoid_opts["goog:chromeOptions"]
349
+
350
+ # Другие опции первого уровня
351
+ for k, v in selenoid_opts.items():
352
+ if k not in caps:
353
+ caps[k] = v
354
+
355
+ # --- UNIFICATION: Inject config args into capabilities ---
356
+ common_args = _build_chromium_args(cfg)
357
+
358
+ opts_key = "goog:chromeOptions"
359
+ if "browserName" in caps and "firefox" in caps["browserName"]:
360
+ opts_key = "moz:firefoxOptions"
361
+
362
+ if opts_key not in caps:
363
+ caps[opts_key] = {}
364
+ if "args" not in caps[opts_key]:
365
+ caps[opts_key]["args"] = []
366
+
367
+ # 1. Headless (from config)
368
+ if cfg.get("headless", False):
369
+ if "--headless" not in caps[opts_key]["args"]:
370
+ caps[opts_key]["args"].append("--headless")
371
+
372
+ # 2. Common Args (no_sandbox, disable_features, etc)
373
+ # Avoid duplicates
374
+ existing_args = set(caps[opts_key]["args"])
375
+ for arg in common_args:
376
+ if arg not in existing_args:
377
+ caps[opts_key]["args"].append(arg)
378
+
379
+ # 3. Добавление имени теста
380
+ if self.request and self.request.node:
381
+ caps["name"] = self.request.node.name
382
+ if caps.get("enableVideo"):
383
+ caps["videoName"] = f"{self.request.node.name}.mp4"
384
+
385
+ # Все capabilities оборачиваем в alwaysMatch для W3C
386
+ w3c_caps = {"alwaysMatch": caps}
387
+
388
+ retries = cfg.get("session_retries", 3)
389
+ timeout = cfg.get("session_timeout", 30.0)
390
+
391
+ ws_url, session_id = _create_selenoid_session(
392
+ selenoid_url, w3c_caps, retries=retries, timeout=timeout
393
+ )
394
+
395
+ # Сохраняем для очистки
396
+ if "selenoid_sessions" not in self._resources:
397
+ self._resources["selenoid_sessions"] = []
398
+ self._resources["selenoid_sessions"].append(
399
+ {"url": selenoid_url, "id": session_id}
400
+ )
401
+
402
+ self._resources["browser"] = browser_type.connect_over_cdp(
403
+ ws_url,
404
+ slow_mo=float(cfg.get("slow_mo", 0.0)),
405
+ timeout=float(cfg.get("timeout", 30000.0)),
406
+ )
407
+
408
+ elif ws_endpoint:
240
409
  # Подключение к удалённому браузеру по WebSocket
241
410
  self._resources["browser"] = browser_type.connect_over_cdp(
242
- ws_endpoint
411
+ ws_endpoint,
412
+ slow_mo=float(cfg.get("slow_mo", 0.0)),
413
+ timeout=float(cfg.get("timeout", 30000.0)),
243
414
  )
244
415
  else:
245
416
  # Локальный запуск
246
- args = cfg.get("args") or []
247
- if cfg.get("no_sandbox", False):
248
- args.append("--no-sandbox")
249
-
250
- disable_features = cfg.get("disable_features") or []
251
-
252
- if cfg.get("ignore_https_errors", False):
253
- args.append("--ignore-certificate-errors")
254
- # Отключаем принудительное обновление до HTTPS и связанные с ним предупреждения
255
- if "HttpsUpgrades" not in disable_features:
256
- disable_features.append("HttpsUpgrades")
257
- if "EnforceHttps" not in disable_features:
258
- disable_features.append("EnforceHttps")
259
-
260
- # Для HTTP сайтов отключаем предупреждения о небезопасности
261
- base_url = cfg.get("base_url")
262
- if base_url:
263
- parsed_url = urlparse(base_url)
264
- if parsed_url.scheme == "http":
265
- origin = f"{parsed_url.scheme}://{parsed_url.netloc}"
266
- args.append(
267
- f"--unsafely-treat-insecure-origin-as-secure={origin}"
268
- )
269
-
270
- if disable_features:
271
- features_str = ",".join(disable_features)
272
- args.append(f"--disable-features={features_str}")
417
+ args = _build_chromium_args(cfg)
273
418
 
274
419
  executable_path = cfg.get("executable_path")
420
+ channel = cfg.get("channel")
421
+ slow_mo = float(cfg.get("slow_mo", 0.0))
422
+ timeout = float(cfg.get("timeout", 30000.0))
275
423
 
276
424
  launch_opts = {
277
425
  "headless": cfg.get("headless", True),
278
426
  "executable_path": executable_path,
279
427
  "args": args if args else None,
428
+ "channel": channel,
429
+ "slow_mo": slow_mo,
430
+ "timeout": timeout,
280
431
  }
281
432
  launch_opts = {
282
433
  k: v for k, v in launch_opts.items() if v is not None
@@ -507,7 +658,22 @@ class PersonaDispatcher:
507
658
  for context in self._resources["browser_contexts"].values():
508
659
  context.close()
509
660
  if "browser" in self._resources:
510
- self._resources["browser"].close()
661
+ try:
662
+ self._resources["browser"].close()
663
+ except Exception as e:
664
+ logger.warning(f"Ошибка при закрытии браузера: {e}")
665
+
666
+ # Удаление сессий Selenoid
667
+ if "selenoid_sessions" in self._resources:
668
+ for session in self._resources["selenoid_sessions"]:
669
+ s_url = session["url"]
670
+ s_id = session["id"]
671
+ try:
672
+ del_url = f"{s_url.rstrip('/')}/wd/hub/session/{s_id}"
673
+ logger.info(f"Удаление сессии Selenoid: {s_id}")
674
+ requests.delete(del_url, timeout=5.0)
675
+ except Exception as e:
676
+ logger.warning(f"Ошибка удаления сессии Selenoid {s_id}: {e}")
511
677
  if "playwright" in self._resources:
512
678
  pw = self._resources["playwright"]
513
679
  if not hasattr(pw, "stop") or not callable(pw.stop):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: persona-dsl
3
- Version: 26.1.21.30
3
+ Version: 26.1.21.32
4
4
  Summary: Persona DSL - Framework for implementing Screenplay pattern in Python tests
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -164,15 +164,63 @@ PERSONA_SKILLS = [BaseSkill.BROWSER, BaseSkill.API]
164
164
  ### `config/{env}.yaml`
165
165
  Параметры окружения (dev, test, prod). Выбирается через `--env=test`.
166
166
 
167
+ ### Примеры конфигурации
168
+
169
+ Ниже представлен **полный справочник** всех возможных настроек.
170
+ Большинство параметров унифицированы и работают идентично для **Локального** запуска и **Selenoid**.
171
+
172
+ #### Полный справочник (Global Reference)
173
+
174
+ Этот конфиг покрывает все возможности фреймворка. Неиспользуемые параметры можно опускать.
175
+
167
176
  ```yaml
168
- base_url: "https://my-app.test"
169
- browser:
170
- headless: true
171
- viewport: { width: 1920, height: 1080 }
172
- reporting:
173
- screenshot_on_fail: true
177
+ skills:
178
+ browser:
179
+ default:
180
+ base_url: "https://my-app.test" # URL должен быть внутри навыка
181
+
182
+ # --- Базовые настройки (Basic) ---
183
+ type: chromium # Тип: chromium, firefox, webkit, sberbrowser, msedge
184
+ headless: true # true = без UI (в контейнере тоже работает)
185
+ channel: chrome # (Optional) Канал: chrome, msedge, chrome-beta
186
+ executable_path: "" # (Optional) Кастомный путь к бинарнику
187
+ version: "128.0" # (Optional) Версия (для Selenoid или скачивания)
188
+
189
+ # --- Размеры и Тайминги (Dimensions & Timings) ---
190
+ viewport:
191
+ width: 1920
192
+ height: 1080 # Всегда используйте многострочный формат для читаемости
193
+ slow_mo: 50.0 # Задержка между действиями (мс)
194
+ default_timeout: 30000.0 # Таймаут поиска элементов (мс)
195
+ default_navigation_timeout: 30000.0 # Таймаут загрузки (мс)
196
+
197
+ # --- Аргументы и Флаги (Arguments) ---
198
+ # Применяются и локально, и удаленно (через goog:chromeOptions)
199
+ no_sandbox: true # --no-sandbox
200
+ ignore_https_errors: true # --ignore-certificate-errors
201
+ disable_features: # --disable-features=...
202
+ - SidePanel
203
+ - SberWhatsNew
204
+ args: # Дополнительные аргументы
205
+ - "--start-maximized"
206
+ - "--disable-notifications"
207
+ - "--disable-gpu"
208
+
209
+ # --- Удаленный запуск (Remote / Selenoid) ---
210
+ # Если URL задан, тесты пойдут в облако/контейнер
211
+ selenoid_url: "http://selenoid.company.com:4444"
212
+
213
+ session_retries: 3 # Повторные попытки при ошибке создания сессии
214
+ session_timeout: 60.0 # Таймаут ожидания слота (сек)
215
+
216
+ selenoid_options:
217
+ enableVNC: true # Включить Live-просмотр
218
+ enableVideo: false # Включить запись видео
219
+ # name/videoName проставляются автоматически
174
220
  ```
175
221
 
222
+
223
+
176
224
  ---
177
225
 
178
226
  ## 📊 Отчетность (Allure + TaaS)
@@ -1,6 +1,6 @@
1
1
  persona_dsl/__init__.py,sha256=C47lvwHDzdM-fsLOy9knGvB5tbqCS9c_rO1SnG7_sc4,875
2
2
  persona_dsl/persona.py,sha256=tQXb6BOYKIQvny-0ry06kneus-jZYyhKwrLq0sjQzks,5278
3
- persona_dsl/pytest_plugin.py,sha256=CyklaxWz3jZnve4FOIX-czOKK4HVHY07zC9OGvpDRco,47044
3
+ persona_dsl/pytest_plugin.py,sha256=lg8raO30kxNiudlJ_SwHMglWXNrnZH_xDuHgcuCDtXs,54527
4
4
  persona_dsl/components/action.py,sha256=z456GYCf10hR1tIYeC7IN1V-7bn5uY1ZwNk053sWEls,218
5
5
  persona_dsl/components/base_step.py,sha256=nZJUKu5oHsOhiAW3g7_K82fGXjkg3HMnSLvKdacBHbs,9661
6
6
  persona_dsl/components/combined_step.py,sha256=-SEKei_1qFDbnQvtKJBN00MJNnM_bMIPp6X0lhZrSbw,3598
@@ -79,8 +79,8 @@ persona_dsl/utils/path.py,sha256=wwYUS3euYqwKQdxWHD8Lq93Ln1G0M5LIJ6AMqiwpmDI,725
79
79
  persona_dsl/utils/retry.py,sha256=kBbu6rpySdeaqRzUkgdrsEWrc9PwkvRhYtRzyhrYj9g,2215
80
80
  persona_dsl/utils/taas_integration.py,sha256=OZzSjgC1_90eTYXo3WPsCgCtYc35U216SBnxClpdIkw,5046
81
81
  persona_dsl/utils/waits.py,sha256=I9e_9rs_0Rju61yx_hS-qwcXd4zNWfVNyfhY0mOHrj0,4386
82
- persona_dsl-26.1.21.30.dist-info/METADATA,sha256=C4C719iREiBXApsPE2XMj1rYl6J14xFHn5vSwKZymoQ,7049
83
- persona_dsl-26.1.21.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
84
- persona_dsl-26.1.21.30.dist-info/entry_points.txt,sha256=8lYko6uS1gGeYX9_FYbbeO5XV_BP62T7RngyKuKDD7k,180
85
- persona_dsl-26.1.21.30.dist-info/top_level.txt,sha256=N1YAJab5h4iPt-srbrjzDk6bY40bwR1AHZS5Z0eOzvM,12
86
- persona_dsl-26.1.21.30.dist-info/RECORD,,
82
+ persona_dsl-26.1.21.32.dist-info/METADATA,sha256=EUZul9vg5dJ8kvUBVkBXNV9SVnDESKEgc3_QB0eoHRQ,9879
83
+ persona_dsl-26.1.21.32.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
84
+ persona_dsl-26.1.21.32.dist-info/entry_points.txt,sha256=8lYko6uS1gGeYX9_FYbbeO5XV_BP62T7RngyKuKDD7k,180
85
+ persona_dsl-26.1.21.32.dist-info/top_level.txt,sha256=N1YAJab5h4iPt-srbrjzDk6bY40bwR1AHZS5Z0eOzvM,12
86
+ persona_dsl-26.1.21.32.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5