Promium 3.7.4__tar.gz → 3.7.5__tar.gz

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 (60) hide show
  1. {promium-3.7.4 → promium-3.7.5}/PKG-INFO +1 -1
  2. {promium-3.7.4 → promium-3.7.5}/Promium.egg-info/PKG-INFO +1 -1
  3. {promium-3.7.4 → promium-3.7.5}/promium/_diag.py +1 -1
  4. {promium-3.7.4 → promium-3.7.5}/promium/assertions.py +383 -81
  5. {promium-3.7.4 → promium-3.7.5}/promium/plugin.py +107 -93
  6. {promium-3.7.4 → promium-3.7.5}/setup.py +1 -1
  7. {promium-3.7.4 → promium-3.7.5}/LICENSE +0 -0
  8. {promium-3.7.4 → promium-3.7.5}/Promium.egg-info/SOURCES.txt +0 -0
  9. {promium-3.7.4 → promium-3.7.5}/Promium.egg-info/dependency_links.txt +0 -0
  10. {promium-3.7.4 → promium-3.7.5}/Promium.egg-info/entry_points.txt +0 -0
  11. {promium-3.7.4 → promium-3.7.5}/Promium.egg-info/requires.txt +0 -0
  12. {promium-3.7.4 → promium-3.7.5}/Promium.egg-info/top_level.txt +0 -0
  13. {promium-3.7.4 → promium-3.7.5}/README.rst +0 -0
  14. {promium-3.7.4 → promium-3.7.5}/doc/__init__.py +0 -0
  15. {promium-3.7.4 → promium-3.7.5}/doc/conf.py +0 -0
  16. {promium-3.7.4 → promium-3.7.5}/promium/__init__.py +0 -0
  17. {promium-3.7.4 → promium-3.7.5}/promium/base.py +0 -0
  18. {promium-3.7.4 → promium-3.7.5}/promium/browsers.py +0 -0
  19. {promium-3.7.4 → promium-3.7.5}/promium/common.py +0 -0
  20. {promium-3.7.4 → promium-3.7.5}/promium/device_config.py +0 -0
  21. {promium-3.7.4 → promium-3.7.5}/promium/elements/__init__.py +0 -0
  22. {promium-3.7.4 → promium-3.7.5}/promium/elements/checkbox.py +0 -0
  23. {promium-3.7.4 → promium-3.7.5}/promium/elements/drop_downs.py +0 -0
  24. {promium-3.7.4 → promium-3.7.5}/promium/elements/frame.py +0 -0
  25. {promium-3.7.4 → promium-3.7.5}/promium/elements/input_field.py +0 -0
  26. {promium-3.7.4 → promium-3.7.5}/promium/elements/link.py +0 -0
  27. {promium-3.7.4 → promium-3.7.5}/promium/elements/multiselect_listbox.py +0 -0
  28. {promium-3.7.4 → promium-3.7.5}/promium/elements/radio_button.py +0 -0
  29. {promium-3.7.4 → promium-3.7.5}/promium/elements/textarea_field.py +0 -0
  30. {promium-3.7.4 → promium-3.7.5}/promium/elements/upload_button.py +0 -0
  31. {promium-3.7.4 → promium-3.7.5}/promium/exceptions.py +0 -0
  32. {promium-3.7.4 → promium-3.7.5}/promium/expected_conditions.py +0 -0
  33. {promium-3.7.4 → promium-3.7.5}/promium/helpers.py +0 -0
  34. {promium-3.7.4 → promium-3.7.5}/promium/logger.py +0 -0
  35. {promium-3.7.4 → promium-3.7.5}/promium/support/__init__.py +0 -0
  36. {promium-3.7.4 → promium-3.7.5}/promium/support/settings.py +0 -0
  37. {promium-3.7.4 → promium-3.7.5}/promium/test_case.py +0 -0
  38. {promium-3.7.4 → promium-3.7.5}/promium/waits.py +0 -0
  39. {promium-3.7.4 → promium-3.7.5}/pyproject.toml +0 -0
  40. {promium-3.7.4 → promium-3.7.5}/setup.cfg +0 -0
  41. {promium-3.7.4 → promium-3.7.5}/tests/__init__.py +0 -0
  42. {promium-3.7.4 → promium-3.7.5}/tests/config.py +0 -0
  43. {promium-3.7.4 → promium-3.7.5}/tests/conftest.py +0 -0
  44. {promium-3.7.4 → promium-3.7.5}/tests/pages/__init__.py +0 -0
  45. {promium-3.7.4 → promium-3.7.5}/tests/pages/catalog_404_page.py +0 -0
  46. {promium-3.7.4 → promium-3.7.5}/tests/pages/catalog_gallery_page.py +0 -0
  47. {promium-3.7.4 → promium-3.7.5}/tests/settings/__init__.py +0 -0
  48. {promium-3.7.4 → promium-3.7.5}/tests/settings/test_base.py +0 -0
  49. {promium-3.7.4 → promium-3.7.5}/tests/settings/test_chrome.py +0 -0
  50. {promium-3.7.4 → promium-3.7.5}/tests/settings/test_edge.py +0 -0
  51. {promium-3.7.4 → promium-3.7.5}/tests/settings/test_firefox.py +0 -0
  52. {promium-3.7.4 → promium-3.7.5}/tests/settings/test_ie.py +0 -0
  53. {promium-3.7.4 → promium-3.7.5}/tests/settings/test_opera.py +0 -0
  54. {promium-3.7.4 → promium-3.7.5}/tests/test_catalog_404.py +0 -0
  55. {promium-3.7.4 → promium-3.7.5}/tests/test_catalog_gallery.py +0 -0
  56. {promium-3.7.4 → promium-3.7.5}/tests/test_enviroment.py +0 -0
  57. {promium-3.7.4 → promium-3.7.5}/tests/test_requests.py +0 -0
  58. {promium-3.7.4 → promium-3.7.5}/tests/test_soft_asserts.py +0 -0
  59. {promium-3.7.4 → promium-3.7.5}/tests/test_waits.py +0 -0
  60. {promium-3.7.4 → promium-3.7.5}/tests/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Promium
3
- Version: 3.7.4
3
+ Version: 3.7.5
4
4
  Summary: Selenium wrapper for testing Web UI
5
5
  Author: Denis Korytkin, Nataliia Guieva, Roman Zaporozhets, Vladimir Kritov, Oleh Dykusha
6
6
  Project-URL: Home page, https://none
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Promium
3
- Version: 3.7.4
3
+ Version: 3.7.5
4
4
  Summary: Selenium wrapper for testing Web UI
5
5
  Author: Denis Korytkin, Nataliia Guieva, Roman Zaporozhets, Vladimir Kritov, Oleh Dykusha
6
6
  Project-URL: Home page, https://none
@@ -322,4 +322,4 @@ def build_digest(
322
322
  if origin and origin[0]:
323
323
  blocks.append(f"↳ locator at {_rel(origin[0])}:{origin[1] or '?'}")
324
324
 
325
- return "\n\n".join(blocks)
325
+ return "\n" + "\n\n".join(blocks)
@@ -9,7 +9,7 @@ from typing import Any, TypeVar
9
9
  import allure
10
10
  import urllib3
11
11
  from json_checker import Checker, CheckerError
12
- from selenium.webdriver.chrome.webdriver import WebDriver
12
+ from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver
13
13
 
14
14
  from promium.common import upload_screenshot
15
15
 
@@ -73,11 +73,26 @@ def get_text_with_ignore_whitespace_symbols(text: str) -> str:
73
73
  return " ".join(text_list_without_space)
74
74
 
75
75
 
76
+ class PromiumAssertionError(AssertionError):
77
+ """Immediate failure with Promium formatting."""
78
+
79
+
76
80
  class BaseSoftAssertion:
77
81
  """Base class for soft assertion utilities."""
78
82
 
79
- # TODO is not cleaned in unit tests need use __init__
80
- assertion_errors = []
83
+ assertion_errors: list[str] = []
84
+
85
+ def _ensure_state(self) -> None:
86
+ """Ensure instance fields exist when __init__ wasn't called."""
87
+ if (
88
+ not hasattr(self, "assertion_errors")
89
+ or self.assertion_errors is None
90
+ ):
91
+ self.assertion_errors = []
92
+ if not hasattr(self, "hard_assert"):
93
+ self.hard_assert = False
94
+ if not hasattr(self, "driver"):
95
+ self.driver = None
81
96
 
82
97
  def _relpath(self, p: str) -> str:
83
98
  """Return relative path from current working directory."""
@@ -96,9 +111,37 @@ class BaseSoftAssertion:
96
111
  f = inspect.stack()[2]
97
112
  return f"↳ at {self._relpath(f.filename)}:{f.lineno}"
98
113
 
114
+ def _format_error(self, error_msg: str, *, with_screen: bool) -> str:
115
+ """Build final error text."""
116
+ screen = self.add_screenshot(error_msg) if with_screen else ""
117
+ return f"{error_msg}{screen}\n{self._callsite_line()}\n"
118
+
119
+ def _emit(
120
+ self,
121
+ error_msg: str,
122
+ with_screen: bool,
123
+ hard_assert_override: bool | None,
124
+ ) -> None:
125
+ self._ensure_state()
126
+ formatted = self._format_error(error_msg, with_screen=with_screen)
127
+ effective_hard_assert = (
128
+ self.hard_assert
129
+ if hard_assert_override is None
130
+ else hard_assert_override
131
+ )
132
+
133
+ if effective_hard_assert:
134
+ __tracebackhide__ = True
135
+ title = "Hard Assertion Error"
136
+ message = f"{title}\n\n{formatted}"
137
+ raise AssertionError(message)
138
+
139
+ self.assertion_errors.append(formatted)
140
+
99
141
  def add_screenshot(self, error_msg: str = "soft_assertion") -> str:
100
142
  """Attach screenshot to allure report if driver is available."""
101
- if hasattr(self, "driver"):
143
+ self._ensure_state()
144
+ if getattr(self, "driver", None):
102
145
  try:
103
146
  allure.attach(
104
147
  self.driver.get_screenshot_as_png(),
@@ -111,36 +154,48 @@ class BaseSoftAssertion:
111
154
  return ""
112
155
 
113
156
  def soft_assert_true(
114
- self, expr: Any, msg: str = None, with_screen: bool = False
157
+ self,
158
+ expr: Any,
159
+ msg: str | None = None,
160
+ *,
161
+ with_screen: bool = False,
162
+ hard_assert: bool | None = None,
115
163
  ) -> str | None:
116
164
  """Check that the expression is true."""
117
165
  if not expr:
118
166
  error = msg or "Is not true."
119
- screen = self.add_screenshot(error) if with_screen else ""
120
- self.assertion_errors.append(
121
- f"{error}{screen}\n{self._callsite_line()}\n"
167
+ self._emit(
168
+ error, with_screen=with_screen, hard_assert_override=hard_assert
122
169
  )
123
170
  return error
171
+ return None
124
172
 
125
173
  def soft_assert_false(
126
- self, expr: Any, msg: str = None, with_screen: bool = False
174
+ self,
175
+ expr: Any,
176
+ msg: str | None = None,
177
+ *,
178
+ with_screen: bool = False,
179
+ hard_assert: bool | None = None,
127
180
  ) -> str | None:
128
181
  """Check that the expression is false."""
129
182
  if expr:
130
183
  error = msg or "Is not false."
131
- screen = self.add_screenshot(error) if with_screen else ""
132
- self.assertion_errors.append(
133
- f"{error}{screen}\n{self._callsite_line()}\n"
184
+ self._emit(
185
+ error, with_screen=with_screen, hard_assert_override=hard_assert
134
186
  )
135
187
  return error
188
+ return None
136
189
 
137
190
  def soft_assert_equals(
138
191
  self,
139
192
  current: Any,
140
193
  expected: Any,
141
- msg: str = None,
194
+ msg: str | None = None,
195
+ *,
142
196
  show_diff: bool = False,
143
197
  with_screen: bool = False,
198
+ hard_assert: bool | None = None,
144
199
  ) -> str | None:
145
200
  """Check that current equals expected, optionally showing diff."""
146
201
  diff = None
@@ -165,15 +220,20 @@ class BaseSoftAssertion:
165
220
  f"{difference}"
166
221
  )
167
222
  return self.soft_assert_true(
168
- current == expected, assert_message, with_screen
223
+ current == expected,
224
+ assert_message,
225
+ with_screen=with_screen,
226
+ hard_assert=hard_assert,
169
227
  )
170
228
 
171
229
  def soft_assert_not_equals(
172
230
  self,
173
231
  current: Any,
174
232
  expected: Any,
175
- msg: str = None,
233
+ msg: str | None = None,
234
+ *,
176
235
  with_screen: bool = False,
236
+ hard_assert: bool | None = None,
177
237
  ) -> str | None:
178
238
  """Check that current does not equal expected."""
179
239
  message = (
@@ -181,118 +241,219 @@ class BaseSoftAssertion:
181
241
  f"Current - {convert_container(current)}\n"
182
242
  f"Expected - {convert_container(expected)}\n"
183
243
  )
184
- self.soft_assert_false(current == expected, message, with_screen)
244
+ return self.soft_assert_false(
245
+ current == expected,
246
+ message,
247
+ with_screen=with_screen,
248
+ hard_assert=hard_assert,
249
+ )
185
250
 
186
251
  def soft_assert_in(
187
252
  self,
188
253
  member: Any,
189
254
  container: Any,
190
- msg: str = None,
255
+ msg: str | None = None,
256
+ *,
191
257
  with_screen: bool = False,
258
+ hard_assert: bool | None = None,
192
259
  ) -> str | None:
193
260
  """Check that member is in container."""
194
- msg = (
261
+ message = (
195
262
  f"{base_msg(msg)}\n"
196
263
  f"Member: '{member}'\n not found in\n"
197
264
  f"Container: '{convert_container(container)}'\n"
198
265
  )
199
- return self.soft_assert_true(member in container, msg, with_screen)
266
+ return self.soft_assert_true(
267
+ member in container,
268
+ message,
269
+ with_screen=with_screen,
270
+ hard_assert=hard_assert,
271
+ )
200
272
 
201
273
  def soft_assert_not_in(
202
274
  self,
203
275
  member: Any,
204
276
  container: Any,
205
- msg: str = None,
277
+ msg: str | None = None,
278
+ *,
206
279
  with_screen: bool = False,
280
+ hard_assert: bool | None = None,
207
281
  ) -> str | None:
208
282
  """Check that member is not in container."""
209
- msg = (
283
+ message = (
210
284
  f"{base_msg(msg)}\n"
211
285
  f"Member: '{member}'\n unexpectedly found in\n"
212
286
  f"Container: '{convert_container(container)}'\n"
213
287
  )
214
- return self.soft_assert_true(member not in container, msg, with_screen)
288
+ return self.soft_assert_true(
289
+ member not in container,
290
+ message,
291
+ with_screen=with_screen,
292
+ hard_assert=hard_assert,
293
+ )
215
294
 
216
295
  def soft_assert_less_equal(
217
- self, a: Any, b: Any, msg: str = None, with_screen: bool = False
296
+ self,
297
+ a: Any,
298
+ b: Any,
299
+ msg: str | None = None,
300
+ *,
301
+ with_screen: bool = False,
302
+ hard_assert: bool | None = None,
218
303
  ) -> str | None:
219
304
  """Check that a is less than or equal to b."""
220
305
  error = f"{base_msg(msg)}{a} not less than or equal to {b}\n"
221
- return self.soft_assert_true(a <= b, error, with_screen)
306
+ return self.soft_assert_true(
307
+ a <= b, error, with_screen=with_screen, hard_assert=hard_assert
308
+ )
222
309
 
223
310
  def soft_assert_less(
224
- self, a: Any, b: Any, msg: str = None, with_screen: bool = False
311
+ self,
312
+ a: Any,
313
+ b: Any,
314
+ msg: str | None = None,
315
+ *,
316
+ with_screen: bool = False,
317
+ hard_assert: bool | None = None,
225
318
  ) -> str | None:
226
319
  """Check that a is less than b."""
227
320
  error = f"{base_msg(msg)}{a} not less than {b}\n"
228
- return self.soft_assert_true(a < b, error, with_screen)
321
+ return self.soft_assert_true(
322
+ a < b, error, with_screen=with_screen, hard_assert=hard_assert
323
+ )
229
324
 
230
325
  def soft_assert_greater_equal(
231
- self, a: Any, b: Any, msg: str = None, with_screen: bool = False
326
+ self,
327
+ a: Any,
328
+ b: Any,
329
+ msg: str | None = None,
330
+ *,
331
+ with_screen: bool = False,
332
+ hard_assert: bool | None = None,
232
333
  ) -> str | None:
233
334
  """Check that a is greater than or equal to b."""
234
335
  error = f"{base_msg(msg)}{a} not greater than or equal to {b}\n"
235
- return self.soft_assert_true(a >= b, error, with_screen)
336
+ return self.soft_assert_true(
337
+ a >= b, error, with_screen=with_screen, hard_assert=hard_assert
338
+ )
236
339
 
237
340
  def soft_assert_greater(
238
- self, a: Any, b: Any, msg: str = None, with_screen: bool = False
341
+ self,
342
+ a: Any,
343
+ b: Any,
344
+ msg: str | None = None,
345
+ *,
346
+ with_screen: bool = False,
347
+ hard_assert: bool | None = None,
239
348
  ) -> str | None:
240
349
  """Check that a is greater than b."""
241
350
  error = f"{base_msg(msg)}{a} not greater than {b}\n"
242
- return self.soft_assert_true(a > b, error, with_screen)
351
+ return self.soft_assert_true(
352
+ a > b, error, with_screen=with_screen, hard_assert=hard_assert
353
+ )
243
354
 
244
355
  def soft_assert_regexp_matches(
245
- self, text: str, expected_regexp: str, msg: str = None
356
+ self,
357
+ text: str,
358
+ expected_regexp: str,
359
+ msg: str | None = None,
360
+ *,
361
+ with_screen: bool = False,
362
+ hard_assert: bool | None = None,
246
363
  ) -> str | None:
247
364
  """Fail the test unless the text matches the regular expression."""
248
365
  pattern = re.compile(expected_regexp)
249
366
  result = pattern.search(text)
250
-
251
367
  if not result:
252
368
  error = (
253
369
  f"{base_msg(msg)}"
254
370
  f"Regexp didn't match."
255
371
  f"Pattern {str(pattern.pattern)} not found in {str(text)}\n"
256
372
  )
257
- self.assertion_errors.append(f"{error}\n{self._callsite_line()}\n")
373
+ self._emit(
374
+ error, with_screen=with_screen, hard_assert_override=hard_assert
375
+ )
258
376
  return error
377
+ return None
259
378
 
260
379
  def soft_assert_disable(
261
- self, element: Any, msg: str = None, with_screen: bool = False
262
- ) -> str:
380
+ self,
381
+ element: Any,
382
+ msg: str | None = None,
383
+ *,
384
+ with_screen: bool = False,
385
+ hard_assert: bool | None = None,
386
+ ) -> str | None:
263
387
  """Check that element has 'disabled' attribute."""
264
388
  default_msg = "" if msg else f"Not disabled {element}\n"
265
389
  error = f"{base_msg(msg)}{default_msg}"
266
390
  return self.soft_assert_true(
267
- element.get_attribute("disabled"), error, with_screen
391
+ element.get_attribute("disabled"),
392
+ error,
393
+ with_screen=with_screen,
394
+ hard_assert=hard_assert,
268
395
  )
269
396
 
270
- def soft_assert_is_none(self, obj: Any, msg: str = None) -> str:
397
+ def soft_assert_is_none(
398
+ self,
399
+ obj: Any,
400
+ msg: str | None = None,
401
+ *,
402
+ with_screen: bool = False,
403
+ hard_assert: bool | None = None,
404
+ ) -> str | None:
271
405
  """Check that object is None."""
272
406
  default_msg = "" if msg else f"{obj} is not None.\n"
273
407
  error = f"{base_msg(msg)}{default_msg}"
274
- return self.soft_assert_true(obj is None, error)
408
+ return self.soft_assert_true(
409
+ obj is None, error, with_screen=with_screen, hard_assert=hard_assert
410
+ )
275
411
 
276
- def soft_assert_is_not_none(self, obj: Any, msg: str = None) -> str:
412
+ def soft_assert_is_not_none(
413
+ self,
414
+ obj: Any,
415
+ msg: str | None = None,
416
+ *,
417
+ with_screen: bool = False,
418
+ hard_assert: bool | None = None,
419
+ ) -> str | None:
277
420
  """Check that object is not None."""
278
421
  default_msg = "" if msg else "Unexpectedly None.\n"
279
422
  error = f"{base_msg(msg)}{default_msg}"
280
- return self.soft_assert_true(obj is not None, error)
423
+ return self.soft_assert_true(
424
+ obj is not None,
425
+ error,
426
+ with_screen=with_screen,
427
+ hard_assert=hard_assert,
428
+ )
281
429
 
282
430
  def soft_assert_is_instance(
283
- self, obj: Any, cls: Any, msg: str = None
284
- ) -> str:
431
+ self,
432
+ obj: Any,
433
+ cls: Any,
434
+ msg: str | None = None,
435
+ *,
436
+ with_screen: bool = False,
437
+ hard_assert: bool | None = None,
438
+ ) -> str | None:
285
439
  """Check that object is instance of given class."""
286
440
  default_msg = "" if msg else f"{obj} is not an instance of {cls}.\n"
287
441
  error = f"{base_msg(msg)}{default_msg}"
288
- return self.soft_assert_true(isinstance(obj, cls), error)
442
+ return self.soft_assert_true(
443
+ isinstance(obj, cls),
444
+ error,
445
+ with_screen=with_screen,
446
+ hard_assert=hard_assert,
447
+ )
289
448
 
290
449
  def soft_assert_equals_text_with_ignore_spaces_and_register(
291
450
  self,
292
451
  current_text: str,
293
452
  expected_text: str,
294
453
  msg: str = "Invalid checked text.",
454
+ *,
295
455
  with_screen: bool = False,
456
+ hard_assert: bool | None = None,
296
457
  ) -> None:
297
458
  """Compare texts ignoring spaces and case."""
298
459
  current = get_text_with_ignore_whitespace_symbols(current_text)
@@ -304,26 +465,40 @@ class BaseSoftAssertion:
304
465
  expected.lower(),
305
466
  f"{msg}\nCurrent text without formatting: {current_text}"
306
467
  f"\nExpected text without formatting: {expected_text}",
468
+ show_diff=False,
307
469
  with_screen=with_screen,
470
+ hard_assert=hard_assert,
308
471
  )
309
472
 
310
473
  def soft_assert_schemas(
311
- self, current: dict, expected: dict, msg: str = ""
474
+ self,
475
+ current: dict,
476
+ expected: dict,
477
+ msg: str = "",
478
+ *,
479
+ with_screen: bool = False,
480
+ hard_assert: bool | None = None,
312
481
  ) -> str | None:
313
482
  """Validate dictionary schema using json_checker."""
314
483
  try:
315
484
  Checker(expected, soft=True).validate(current)
316
485
  except CheckerError as e:
317
486
  error = f"{msg}\n{e}\n"
318
- self.assertion_errors.append(f"{error}\n{self._callsite_line()}\n")
487
+ self._emit(
488
+ error, with_screen=with_screen, hard_assert_override=hard_assert
489
+ )
319
490
  return error
491
+ return None
320
492
 
321
493
  def assert_keys_and_instances(
322
494
  self,
323
495
  actual_dict: dict,
324
496
  expected_dict: dict,
325
- can_be_null: list = None,
326
- msg: str = None,
497
+ can_be_null: list | None = None,
498
+ msg: str | None = None,
499
+ *,
500
+ with_screen: bool = False,
501
+ hard_assert: bool | None = None,
327
502
  ) -> None:
328
503
  """Check that dict keys and value types match expected."""
329
504
  assert actual_dict, "Actual dict is empty, check your data"
@@ -332,12 +507,16 @@ class BaseSoftAssertion:
332
507
  sorted(iter(actual_dict.keys())),
333
508
  sorted(iter(expected_dict.keys())),
334
509
  "Wrong keys list.",
510
+ with_screen=with_screen,
511
+ hard_assert=hard_assert,
335
512
  )
336
513
  for actual_key, actual_value in actual_dict.items():
337
514
  self.soft_assert_in(
338
515
  member=actual_key,
339
516
  container=expected_dict,
340
517
  msg=f'Not expected key "{actual_key}".',
518
+ with_screen=with_screen,
519
+ hard_assert=hard_assert,
341
520
  )
342
521
  if actual_key in expected_dict:
343
522
  expected_value = (
@@ -353,10 +532,18 @@ class BaseSoftAssertion:
353
532
  f'Key "{actual_key}" value is "{type(actual_value)}", '
354
533
  f'expected "{expected_value}". {message}'
355
534
  ),
535
+ with_screen=with_screen,
536
+ hard_assert=hard_assert,
356
537
  )
357
538
 
358
539
  def soft_assert_dicts_with_ignore_types(
359
- self, current: Any, expected: Any, msg: str = ""
540
+ self,
541
+ current: Any,
542
+ expected: Any,
543
+ msg: str = "",
544
+ *,
545
+ with_screen: bool = False,
546
+ hard_assert: bool | None = None,
360
547
  ) -> None:
361
548
  """Compare two dicts ignoring value types."""
362
549
  expected_dict = _check_namedtuple(expected)
@@ -371,19 +558,24 @@ class BaseSoftAssertion:
371
558
  f"Expected: {expected_dict[actual_key]} \n"
372
559
  )
373
560
  if errors:
374
- self.assertion_errors.append(
561
+ error = (
375
562
  f"{msg}{errors}"
376
563
  f"\nExpected dict: {convert_container(expected_dict)}"
377
564
  f"\nCurrent dict: {convert_container(current_dict)}"
378
- f"\n{self._callsite_line()}\n"
565
+ )
566
+ self._emit(
567
+ error, with_screen=with_screen, hard_assert_override=hard_assert
379
568
  )
380
569
 
381
570
  def soft_assert_equal_dict_from_clerk_magic(
382
571
  self,
383
572
  current_dict: dict,
384
573
  expected_dict: dict,
385
- msg: str = None,
574
+ msg: str | None = None,
386
575
  coerce: CoerceDict = None,
576
+ *,
577
+ with_screen: bool = False,
578
+ hard_assert: bool | None = None,
387
579
  ) -> None:
388
580
  """Compare dicts by type, coercing values if needed."""
389
581
 
@@ -418,6 +610,8 @@ class BaseSoftAssertion:
418
610
  f"Unequal_keys:\n\t{unequal_check}\n"
419
611
  f"\nMissed_keys:\n\t[{missing_check}]"
420
612
  ),
613
+ with_screen=with_screen,
614
+ hard_assert=hard_assert,
421
615
  )
422
616
 
423
617
 
@@ -426,9 +620,10 @@ class RequestSoftAssertion(BaseSoftAssertion):
426
620
 
427
621
  @property
428
622
  def url(self) -> str:
623
+ """Return current URL from session."""
429
624
  return self.session.url
430
625
 
431
- def base_msg(self, msg: str = None) -> str:
626
+ def base_msg(self, msg: str | None = None) -> str:
432
627
  """Return formatted message including URL and optional extra message."""
433
628
  url = f"\n{self.url}" if self.url else ""
434
629
  exception = f"\n{msg}" if msg else ""
@@ -438,54 +633,112 @@ class RequestSoftAssertion(BaseSoftAssertion):
438
633
  class WebDriverSoftAssertion(BaseSoftAssertion):
439
634
  """Soft assertions for WebDriver-based checks."""
440
635
 
636
+ _driver: SeleniumWebDriver | None = None
637
+
441
638
  @property
442
- def driver(self) -> WebDriver:
443
- return self.driver
639
+ def driver(self) -> SeleniumWebDriver | None:
640
+ return self._driver
641
+
642
+ @driver.setter
643
+ def driver(self, value: SeleniumWebDriver | None) -> None:
644
+ self._driver = value
444
645
 
445
646
  @property
446
647
  def url(self) -> str:
447
- return self.driver.current_url
648
+ """Return current browser URL."""
649
+ return self.driver.current_url if self.driver else ""
448
650
 
449
- def base_msg(self, default_msg: str = None, extend_msg: str = None) -> str:
651
+ def base_msg(
652
+ self, default_msg: str | None = None, extend_msg: str | None = None
653
+ ) -> str:
450
654
  """Return formatted message including default, extended, and URL."""
451
655
  default = f"Default message: {default_msg}\n" if default_msg else ""
452
656
  extend = f"Extend message: {extend_msg}\n" if extend_msg else ""
453
657
  return f"{default}{extend}\nURL: {self.url}"
454
658
 
455
659
  def soft_assert_page_title(
456
- self, expected_title: str, msg: str = None
660
+ self,
661
+ expected_title: str,
662
+ msg: str | None = None,
663
+ *,
664
+ with_screen: bool = False,
665
+ hard_assert: bool | None = None,
457
666
  ) -> None:
458
667
  """Check that page title matches expected."""
459
668
  default_msg = "Wrong page title."
460
669
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
461
- self.soft_assert_equals(self.driver.title, expected_title, error)
670
+ self.soft_assert_equals(
671
+ self.driver.title if self.driver else None,
672
+ expected_title,
673
+ error,
674
+ with_screen=with_screen,
675
+ hard_assert=hard_assert,
676
+ )
462
677
 
463
678
  def soft_assert_current_url(
464
- self, expected_url: str, msg: str = None
679
+ self,
680
+ expected_url: str,
681
+ msg: str | None = None,
682
+ *,
683
+ with_screen: bool = False,
684
+ hard_assert: bool | None = None,
465
685
  ) -> None:
466
686
  """Check that current URL matches expected."""
467
687
  default_msg = "Wrong current url."
468
688
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
469
- self.soft_assert_equals(self.url, expected_url, error)
689
+ self.soft_assert_equals(
690
+ self.url,
691
+ expected_url,
692
+ error,
693
+ with_screen=with_screen,
694
+ hard_assert=hard_assert,
695
+ )
470
696
 
471
697
  def soft_assert_current_url_contains(
472
- self, url_mask: str, msg: str = None
698
+ self,
699
+ url_mask: str,
700
+ msg: str | None = None,
701
+ *,
702
+ with_screen: bool = False,
703
+ hard_assert: bool | None = None,
473
704
  ) -> None:
474
705
  """Check that current URL contains mask."""
475
706
  default_msg = f"URL {str(self.url)} doesn't contain {str(url_mask)}."
476
707
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
477
- self.soft_assert_in(url_mask, self.url, error)
708
+ self.soft_assert_in(
709
+ url_mask,
710
+ self.url,
711
+ error,
712
+ with_screen=with_screen,
713
+ hard_assert=hard_assert,
714
+ )
478
715
 
479
716
  def soft_assert_current_url_not_contains(
480
- self, url_mask: str, msg: str = None
717
+ self,
718
+ url_mask: str,
719
+ msg: str | None = None,
720
+ *,
721
+ with_screen: bool = False,
722
+ hard_assert: bool | None = None,
481
723
  ) -> None:
482
724
  """Check that current URL does not contain mask."""
483
725
  default_msg = f"URL {str(self.url)} contains {str(url_mask)}."
484
726
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
485
- self.soft_assert_not_in(url_mask, self.url, error)
727
+ self.soft_assert_not_in(
728
+ url_mask,
729
+ self.url,
730
+ error,
731
+ with_screen=with_screen,
732
+ hard_assert=hard_assert,
733
+ )
486
734
 
487
735
  def soft_assert_element_is_present(
488
- self, element: Any, msg: str = None
736
+ self,
737
+ element: Any,
738
+ msg: str | None = None,
739
+ *,
740
+ with_screen: bool = True,
741
+ hard_assert: bool | None = None,
489
742
  ) -> None:
490
743
  """Check that element is present in DOM."""
491
744
  default_msg = (
@@ -493,38 +746,78 @@ class WebDriverSoftAssertion(BaseSoftAssertion):
493
746
  f" on page at current time."
494
747
  )
495
748
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
496
- self.soft_assert_true(element.is_present(), error, with_screen=True)
749
+ self.soft_assert_true(
750
+ element.is_present(),
751
+ error,
752
+ with_screen=with_screen,
753
+ hard_assert=hard_assert,
754
+ )
497
755
 
498
756
  def soft_assert_element_is_not_present(
499
- self, element: Any, msg: str = None
757
+ self,
758
+ element: Any,
759
+ msg: str | None = None,
760
+ *,
761
+ with_screen: bool = True,
762
+ hard_assert: bool | None = None,
500
763
  ) -> None:
501
764
  """Check that element is not present in DOM."""
502
765
  default_msg = f"Element {element.by}={element.locator} is found on page."
503
766
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
504
- self.soft_assert_false(element.is_present(), error, with_screen=True)
767
+ self.soft_assert_false(
768
+ element.is_present(),
769
+ error,
770
+ with_screen=with_screen,
771
+ hard_assert=hard_assert,
772
+ )
505
773
 
506
774
  def soft_assert_element_is_displayed(
507
- self, element: Any, msg: str = None
775
+ self,
776
+ element: Any,
777
+ msg: str | None = None,
778
+ *,
779
+ with_screen: bool = True,
780
+ hard_assert: bool | None = None,
508
781
  ) -> None:
509
782
  """Check that element is visible to user."""
510
783
  default_msg = (
511
784
  f"Element {element.by}={element.locator} is not visible to a user."
512
785
  )
513
786
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
514
- self.soft_assert_true(element.is_displayed(), error, with_screen=True)
787
+ self.soft_assert_true(
788
+ element.is_displayed(),
789
+ error,
790
+ with_screen=with_screen,
791
+ hard_assert=hard_assert,
792
+ )
515
793
 
516
794
  def soft_assert_element_is_not_displayed(
517
- self, element: Any, msg: str = None
795
+ self,
796
+ element: Any,
797
+ msg: str | None = None,
798
+ *,
799
+ with_screen: bool = True,
800
+ hard_assert: bool | None = None,
518
801
  ) -> None:
519
802
  """Check that element is not visible to user."""
520
803
  default_msg = (
521
804
  f"Element {element.by}={element.locator} is visible to a user."
522
805
  )
523
806
  error = f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}"
524
- self.soft_assert_false(element.is_displayed(), error, with_screen=True)
807
+ self.soft_assert_false(
808
+ element.is_displayed(),
809
+ error,
810
+ with_screen=with_screen,
811
+ hard_assert=hard_assert,
812
+ )
525
813
 
526
814
  def soft_assert_element_displayed_in_viewport(
527
- self, element: Any, msg: str = None
815
+ self,
816
+ element: Any,
817
+ msg: str | None = None,
818
+ *,
819
+ with_screen: bool = True,
820
+ hard_assert: bool | None = None,
528
821
  ) -> None:
529
822
  """Check that element is inside viewport."""
530
823
  element_in_viewport = self.driver.execute_script(
@@ -565,25 +858,34 @@ class WebDriverSoftAssertion(BaseSoftAssertion):
565
858
  error = (
566
859
  f"{self.base_msg(default_msg=default_msg, extend_msg=msg)}\n\n"
567
860
  )
568
- self.assertion_errors.append(f"{error}\n{self._callsite_line()}\n")
861
+ self._emit(
862
+ error, with_screen=with_screen, hard_assert_override=hard_assert
863
+ )
569
864
 
570
865
  def soft_assert_image_status_code(
571
- self, image: Any, status_code: int = 200, msg: str = None
866
+ self,
867
+ image: Any,
868
+ status_code: int = 200,
869
+ msg: str | None = None,
870
+ *,
871
+ with_screen: bool = False,
872
+ hard_assert: bool | None = None,
572
873
  ) -> str | None:
573
874
  """Check that image URL returns expected status code."""
574
875
  if not image.get_attribute("src"):
575
- return self.assertion_errors.append(
576
- f"{base_msg(msg)}"
577
- "Image does not have attribute"
578
- f" 'src'\n\n{self._callsite_line()}\n"
876
+ error = f"{base_msg(msg)}Image does not have attribute 'src'\n"
877
+ self._emit(
878
+ error, with_screen=with_screen, hard_assert_override=hard_assert
579
879
  )
880
+ return error
580
881
 
581
882
  img_url = image.get_attribute("src")
582
883
  response = http.request(url=img_url, method="GET")
583
-
584
884
  assert_msg = msg if msg else "img status code != 200"
585
- self.soft_assert_equals(
885
+ return self.soft_assert_equals(
586
886
  response.status,
587
887
  status_code,
588
888
  msg=assert_msg,
889
+ with_screen=with_screen,
890
+ hard_assert=hard_assert,
589
891
  )
@@ -4,6 +4,10 @@ import logging
4
4
  import os
5
5
  from _pytest.config import Config
6
6
  from _pytest.runner import CallInfo
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.syntax import Syntax
10
+ from rich.text import Text
7
11
  from typing import NamedTuple
8
12
 
9
13
  import allure
@@ -277,94 +281,79 @@ def color_msg(msg: str, color: str = "bold red") -> str:
277
281
  return f"[{color}]{msg}[/{color}]"
278
282
 
279
283
 
284
+ def _panel_width() -> int:
285
+ env_w = os.getenv("PROMIUM_ASSERTION_PANEL_WIDTH")
286
+ try:
287
+ term_w = os.get_terminal_size().columns
288
+ except Exception:
289
+ term_w = 120
290
+ if env_w is None:
291
+ return term_w
292
+ try:
293
+ w = int(env_w)
294
+ if w < 0 or w > term_w:
295
+ return term_w
296
+ return w
297
+ except Exception:
298
+ return term_w
299
+
300
+
301
+ def _render_panel(text: str, title: str) -> str:
302
+ width = _panel_width()
303
+ border_style = "none" if mono_color else "red"
304
+ buf = io.StringIO()
305
+ console = Console(file=buf, force_terminal=True, width=width - 5)
306
+ msg = (
307
+ Text(text.strip(), justify="left")
308
+ if mono_color
309
+ else Syntax(text.strip(), "sql", word_wrap=True)
310
+ )
311
+ panel = Panel(
312
+ renderable=msg,
313
+ title=color_msg(title),
314
+ border_style=border_style,
315
+ padding=(1, 1),
316
+ )
317
+ console.print(panel)
318
+ return buf.getvalue()
319
+
320
+
280
321
  @pytest.mark.trylast
281
322
  def pytest_runtest_call(item: pytest.Item) -> None:
282
323
  separator = "-" * 46
283
- if PANEL_WIDTH := os.getenv("PROMIUM_ASSERTION_PANEL_WIDTH"):
284
- from rich.console import Console # noqa: PLC0415
285
- from rich.panel import Panel # noqa: PLC0415
286
- from rich.syntax import Syntax # noqa: PLC0415
287
- from rich.text import Text # noqa: PLC0415
288
-
289
- try:
290
- terminal_width = os.get_terminal_size().columns
291
- except Exception:
292
- terminal_width = 120
293
-
294
- try:
295
- PANEL_WIDTH = int(PANEL_WIDTH)
296
- if PANEL_WIDTH < 0 or terminal_width < PANEL_WIDTH:
297
- PANEL_WIDTH = terminal_width
298
- except Exception:
299
- PANEL_WIDTH = terminal_width
300
- if mono_color:
301
- border_style = "none"
302
- else:
303
- border_style = "red"
324
+ use_rich = os.getenv("PROMIUM_ASSERTION_PANEL_WIDTH") is not None
304
325
 
305
326
  if hasattr(item.config, "assertion_errors"):
306
327
  if item.config.assertion_errors:
307
- if PANEL_WIDTH:
308
- string_io = io.StringIO()
309
- console = Console(
310
- file=string_io,
311
- force_terminal=True,
312
- width=PANEL_WIDTH - 5,
328
+ if use_rich:
329
+ rich_output = "".join(
330
+ _render_panel(err.strip(), "Soft Assertion Error")
331
+ for err in item.config.assertion_errors
313
332
  )
314
- for error_text in item.config.assertion_errors:
315
- text = error_text.strip()
316
- if mono_color:
317
- msg = Text(text, justify="left")
318
- else:
319
- msg = Syntax(text, "sql", word_wrap=True)
320
- rich_panel = Panel(
321
- renderable=msg,
322
- title=color_msg("Soft Assertion Error"),
323
- border_style=border_style,
324
- padding=(1, 1),
325
- )
326
- console.print(rich_panel)
327
- rich_output = string_io.getvalue()
328
333
  raise AssertionError(f"\n{rich_output}")
329
- else: # noqa: RET506
330
- assertion_errors_list = f"{separator}\n".join(
331
- item.config.assertion_errors
332
- )
333
- msg = f"\n{separator}\n{assertion_errors_list}{separator}\n"
334
- raise AssertionError(msg)
334
+ assertion_errors_list = f"{separator}\n".join(
335
+ item.config.assertion_errors
336
+ )
337
+ msg = f"\n{separator}\n{assertion_errors_list}{separator}\n"
338
+ raise AssertionError(msg)
335
339
 
336
340
  if item.config.getoption("--check-console"):
337
341
  if hasattr(item.config, "check_console_errors"):
338
342
  if console_errors := item.config.check_console_errors(item):
339
- if PANEL_WIDTH:
340
- string_io = io.StringIO()
341
- console = Console(
342
- file=string_io,
343
- force_terminal=True,
344
- width=PANEL_WIDTH - 5,
345
- )
346
- for error_text in console_errors:
347
- text = error_text.strip()
348
- if mono_color:
349
- msg = Text(text, justify="left")
350
- else:
351
- msg = Syntax(text, "sql", word_wrap=True)
352
- rich_panel = Panel(
353
- renderable=msg,
354
- title=color_msg("Browser console js errors found"),
355
- border_style=border_style,
356
- padding=(1, 1),
343
+ if use_rich:
344
+ rich_output = "".join(
345
+ _render_panel(
346
+ err.strip(), "Browser console js errors found"
357
347
  )
358
- console.print(rich_panel)
359
- rich_output = string_io.getvalue()
360
- raise BrowserConsoleException(f"\n{rich_output}")
361
- else: # noqa: RET506
362
- console_errors_list = f"{separator}\n".join(console_errors)
363
- msg = (
364
- f"Browser console js errors found:"
365
- f"\n{separator}\n{console_errors_list}{separator}\n"
348
+ for err in console_errors
366
349
  )
367
- raise BrowserConsoleException(msg)
350
+ raise BrowserConsoleException(f"\n{rich_output}")
351
+ console_errors_list = f"{separator}\n".join(console_errors)
352
+ msg = (
353
+ f"Browser console js errors found:"
354
+ f"\n{separator}\n{console_errors_list}{separator}\n"
355
+ )
356
+ raise BrowserConsoleException(msg)
368
357
 
369
358
 
370
359
  @pytest.mark.tryfirst
@@ -396,7 +385,6 @@ def pytest_runtest_makereport(item: pytest.Item, call: CallInfo) -> None:
396
385
  name="SCREENSHOT",
397
386
  attachment_type=allure.attachment_type.PNG,
398
387
  )
399
-
400
388
  elif fail_debug.test_type == "request":
401
389
  fail_info = (
402
390
  f"\nURL: {fail_debug.url}"
@@ -423,30 +411,56 @@ def pytest_runtest_makereport(item: pytest.Item, call: CallInfo) -> None:
423
411
  name="RUN COMMAND",
424
412
  attachment_type=allure.attachment_type.HTML,
425
413
  )
426
-
427
414
  except WebDriverException as e:
428
415
  fail_info = f"\nWebdriver instance must have crushed: {e.msg}"
429
416
  finally:
430
417
  longrepr = getattr(report, "longrepr", None)
431
418
  if hasattr(longrepr, "addsection"):
432
- longrepr.sections.insert(
433
- 0, (f"Captured stdout {when}", fail_info, "-")
434
- )
419
+ longrepr.sections.append((
420
+ f"Captured stdout {when}",
421
+ fail_info,
422
+ "-",
423
+ ))
424
+ else:
425
+ report.sections.append((
426
+ f"Captured stdout {when}",
427
+ fail_info,
428
+ ))
429
+
430
+ if call.excinfo is not None:
431
+ exc = call.excinfo.value
432
+ panel_text = getattr(exc, "_promium_panel_text", None)
433
+ if (
434
+ panel_text
435
+ and os.getenv("PROMIUM_ASSERTION_PANEL_WIDTH") is not None
436
+ ):
437
+ title = getattr(
438
+ exc, "_promium_panel_title", "Soft Assertion Error"
439
+ )
440
+ rich_output = _render_panel(panel_text, title)
441
+ report.sections.append((title, "\n" + rich_output))
435
442
 
436
- if item.config.getoption("--duration-time"):
437
- duration = call.stop - call.start
438
- if duration > 120:
439
- report.sections.append((
440
- f"Captured stdout {when}",
441
- (
442
- "\n\n!!!!! The very slow test. "
443
- "Duration time is {} !!!!!\n\n"
444
- ).format(
445
- datetime.datetime.fromtimestamp(duration).strftime(
446
- "%M min %S sec"
447
- )
448
- ),
449
- ))
443
+ if call.when == "teardown" and hasattr(item, "capturelog_handler"):
444
+ root_logger = logging.getLogger()
445
+ try:
446
+ root_logger.removeHandler(item.capturelog_handler)
447
+ finally:
448
+ item.capturelog_handler.close()
449
+ del item.capturelog_handler
450
+
451
+ if item.config.getoption("--duration-time"):
452
+ duration = call.stop - call.start
453
+ if duration > 120:
454
+ report.sections.append((
455
+ f"Captured stdout {when}",
456
+ (
457
+ "\n\n!!!!! The very slow test. Duration time is {} !!!!!\n\n"
458
+ ).format(
459
+ datetime.datetime.fromtimestamp(duration).strftime(
460
+ "%M min %S sec"
461
+ )
462
+ ),
463
+ ))
450
464
 
451
465
 
452
466
  def pytest_configure(config: Config) -> None:
@@ -19,7 +19,7 @@ LONG_DESCRIPTION = Path("README.rst").read_text(encoding="utf-8")
19
19
 
20
20
  setup(
21
21
  name="Promium",
22
- version="3.7.4",
22
+ version="3.7.5",
23
23
  install_requires=REQUIREMENTS,
24
24
  author=(
25
25
  "Denis Korytkin, Nataliia Guieva, "
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes