Promium 3.9.0__tar.gz → 3.9.2__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.
- {promium-3.9.0 → promium-3.9.2}/PKG-INFO +1 -1
- {promium-3.9.0 → promium-3.9.2}/Promium.egg-info/PKG-INFO +1 -1
- {promium-3.9.0 → promium-3.9.2}/Promium.egg-info/SOURCES.txt +1 -0
- promium-3.9.2/promium/_introspect.py +94 -0
- promium-3.9.2/promium/_soft_assert_rewrite.py +249 -0
- {promium-3.9.0 → promium-3.9.2}/promium/assertions.py +3 -2
- {promium-3.9.0 → promium-3.9.2}/promium/base.py +30 -29
- {promium-3.9.0 → promium-3.9.2}/promium/common.py +1 -1
- {promium-3.9.0 → promium-3.9.2}/promium/plugin.py +10 -0
- {promium-3.9.0 → promium-3.9.2}/promium/waits.py +35 -37
- {promium-3.9.0 → promium-3.9.2}/pyproject.toml +2 -0
- {promium-3.9.0 → promium-3.9.2}/setup.py +1 -1
- {promium-3.9.0 → promium-3.9.2}/tests/test_waits.py +6 -6
- promium-3.9.0/promium/_introspect.py +0 -206
- {promium-3.9.0 → promium-3.9.2}/LICENSE +0 -0
- {promium-3.9.0 → promium-3.9.2}/Promium.egg-info/dependency_links.txt +0 -0
- {promium-3.9.0 → promium-3.9.2}/Promium.egg-info/entry_points.txt +0 -0
- {promium-3.9.0 → promium-3.9.2}/Promium.egg-info/requires.txt +0 -0
- {promium-3.9.0 → promium-3.9.2}/Promium.egg-info/top_level.txt +0 -0
- {promium-3.9.0 → promium-3.9.2}/README.rst +0 -0
- {promium-3.9.0 → promium-3.9.2}/doc/__init__.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/doc/conf.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/__init__.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/_diag.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/browsers.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/device_config.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/__init__.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/checkbox.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/drop_downs.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/frame.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/input_field.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/link.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/multiselect_listbox.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/radio_button.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/textarea_field.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/elements/upload_button.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/exceptions.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/expected_conditions.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/helpers.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/logger.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/support/__init__.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/support/settings.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/promium/test_case.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/setup.cfg +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/__init__.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/config.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/conftest.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/pages/__init__.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/pages/catalog_404_page.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/pages/catalog_gallery_page.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/settings/__init__.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/settings/test_base.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/settings/test_chrome.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/settings/test_edge.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/settings/test_firefox.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/settings/test_ie.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/settings/test_opera.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_catalog_404.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_catalog_gallery.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_enviroment.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_exceptions.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_expected_conditions.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_helpers.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_requests.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_soft_asserts.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/test_test_cases.py +0 -0
- {promium-3.9.0 → promium-3.9.2}/tests/urls.py +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Formatting helpers for soft_assert error messages."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import os
|
|
5
|
+
import pprint
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_NO_COLOR = bool(
|
|
9
|
+
os.environ.get("NO_COLOR")
|
|
10
|
+
or os.environ.get("PROMIUM_ASSERTION_PANEL_MONO_COLOR")
|
|
11
|
+
)
|
|
12
|
+
_RED = "" if _NO_COLOR else "\033[31m"
|
|
13
|
+
_GREEN = "" if _NO_COLOR else "\033[32m"
|
|
14
|
+
_YELLOW = "" if _NO_COLOR else "\033[1;33m"
|
|
15
|
+
_BOLD = "" if _NO_COLOR else "\033[1m"
|
|
16
|
+
_WHITE = "" if _NO_COLOR else "\033[1;37m"
|
|
17
|
+
_RESET = "" if _NO_COLOR else "\033[0m"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _escape_control(s: str) -> str:
|
|
21
|
+
"""Replace control characters with \\xNN escapes, keep Unicode as-is."""
|
|
22
|
+
return "".join(
|
|
23
|
+
f"\\x{ord(c):02x}" if ord(c) < 32 or ord(c) == 127 else c for c in s
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _colorize_ndiff(left_lines: list[str], right_lines: list[str]) -> str:
|
|
28
|
+
"""Return colorized ndiff output for two sequences of lines."""
|
|
29
|
+
colored = []
|
|
30
|
+
for line in difflib.ndiff(left_lines, right_lines):
|
|
31
|
+
stripped = line.rstrip("\n")
|
|
32
|
+
if line.startswith("- "):
|
|
33
|
+
colored.append(f"{_RED}{stripped}{_RESET}\n")
|
|
34
|
+
elif line.startswith("+ "):
|
|
35
|
+
colored.append(f"{_GREEN}{stripped}{_RESET}\n")
|
|
36
|
+
elif line.startswith("? "):
|
|
37
|
+
colored.append(f"{_YELLOW}{stripped}{_RESET}\n")
|
|
38
|
+
else:
|
|
39
|
+
colored.append(line)
|
|
40
|
+
return "".join(colored)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _repr_value(val: object) -> str:
|
|
44
|
+
"""Return repr of a value, truncated to 200 chars."""
|
|
45
|
+
r = repr(val)
|
|
46
|
+
return r if len(r) <= 200 else r[:200] + "..."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _to_lines(obj: object) -> list[str]:
|
|
50
|
+
"""Pretty-format *obj* into a list of lines ending with ``\\n``."""
|
|
51
|
+
fmt = pprint.PrettyPrinter(width=1, sort_dicts=False).pformat
|
|
52
|
+
lines = fmt(obj).splitlines(keepends=True)
|
|
53
|
+
if lines and not lines[-1].endswith("\n"):
|
|
54
|
+
lines[-1] += "\n"
|
|
55
|
+
return lines
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def format_sa_data(sa_data: tuple) -> str:
|
|
59
|
+
"""Format captured ``_sa_data`` into a colorized error string."""
|
|
60
|
+
tag, _source, *rest = sa_data
|
|
61
|
+
|
|
62
|
+
if tag == "cmp":
|
|
63
|
+
op, left, right = rest
|
|
64
|
+
result = (
|
|
65
|
+
f"{_RED}{_repr_value(left)}{_RESET} "
|
|
66
|
+
f"{_BOLD}{op}{_RESET} "
|
|
67
|
+
f"{_GREEN}{_repr_value(right)}{_RESET}"
|
|
68
|
+
)
|
|
69
|
+
if (
|
|
70
|
+
op in {"==", "!="}
|
|
71
|
+
and isinstance(left, str)
|
|
72
|
+
and isinstance(right, str)
|
|
73
|
+
):
|
|
74
|
+
esc = _escape_control
|
|
75
|
+
result += "\n" + _colorize_ndiff(
|
|
76
|
+
[esc(left) + "\n"],
|
|
77
|
+
[esc(right) + "\n"],
|
|
78
|
+
)
|
|
79
|
+
elif (
|
|
80
|
+
op in {"==", "!="}
|
|
81
|
+
and isinstance(left, (dict, list, tuple, set, frozenset))
|
|
82
|
+
and isinstance(right, (dict, list, tuple, set, frozenset))
|
|
83
|
+
):
|
|
84
|
+
result += "\n" + _colorize_ndiff(
|
|
85
|
+
_to_lines(left),
|
|
86
|
+
_to_lines(right),
|
|
87
|
+
)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
if tag == "val":
|
|
91
|
+
(value,) = rest
|
|
92
|
+
return f"{_RED}{_repr_value(value)}{_RESET}"
|
|
93
|
+
|
|
94
|
+
return str(sa_data)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""AST rewriter for ``soft_assert`` calls.
|
|
2
|
+
|
|
3
|
+
Transforms ``self.soft_assert(expr, ...)`` at **import time** so that
|
|
4
|
+
sub-expressions are evaluated into temporary variables *before* the
|
|
5
|
+
comparison. The captured values are forwarded to ``soft_assert`` via
|
|
6
|
+
the hidden ``_sa_data`` keyword argument, eliminating the need for
|
|
7
|
+
runtime ``eval()`` and preventing double-evaluation of side-effecting
|
|
8
|
+
expressions like ``list.pop()``.
|
|
9
|
+
|
|
10
|
+
The rewriter is installed as an import hook via :func:`install` (called
|
|
11
|
+
from ``promium.plugin.pytest_configure``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import ast
|
|
16
|
+
import contextlib
|
|
17
|
+
import importlib.abc
|
|
18
|
+
import importlib.machinery
|
|
19
|
+
import importlib.util
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
import types
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_COMPARE_OPS: dict[type, str] = {
|
|
30
|
+
ast.Eq: "==",
|
|
31
|
+
ast.NotEq: "!=",
|
|
32
|
+
ast.Lt: "<",
|
|
33
|
+
ast.LtE: "<=",
|
|
34
|
+
ast.Gt: ">",
|
|
35
|
+
ast.GtE: ">=",
|
|
36
|
+
ast.Is: "is",
|
|
37
|
+
ast.IsNot: "is not",
|
|
38
|
+
ast.In: "in",
|
|
39
|
+
ast.NotIn: "not in",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SoftAssertTransformer(ast.NodeTransformer):
|
|
44
|
+
"""Rewrite ``self.soft_assert(expr, ...)`` into temp-var assignments
|
|
45
|
+
followed by a call that carries ``_sa_data`` with pre-computed values.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, source: str) -> None:
|
|
49
|
+
self._source = source
|
|
50
|
+
self._counter = 0
|
|
51
|
+
self.changed = False
|
|
52
|
+
|
|
53
|
+
def _tmp(self) -> str:
|
|
54
|
+
name = f"@_sa_{self._counter}"
|
|
55
|
+
self._counter += 1
|
|
56
|
+
return name
|
|
57
|
+
|
|
58
|
+
def _assign(self, name: str, value: ast.expr, donor: ast.AST) -> ast.Assign:
|
|
59
|
+
stmt = ast.Assign(
|
|
60
|
+
targets=[ast.Name(id=name, ctx=ast.Store())],
|
|
61
|
+
value=value,
|
|
62
|
+
)
|
|
63
|
+
ast.copy_location(stmt, donor)
|
|
64
|
+
ast.fix_missing_locations(stmt)
|
|
65
|
+
return stmt
|
|
66
|
+
|
|
67
|
+
def _finish(
|
|
68
|
+
self,
|
|
69
|
+
stmts: list[ast.stmt],
|
|
70
|
+
call: ast.Call,
|
|
71
|
+
new_first_arg: ast.expr,
|
|
72
|
+
sa_data: ast.Tuple,
|
|
73
|
+
donor: ast.AST,
|
|
74
|
+
) -> list[ast.stmt]:
|
|
75
|
+
new_call = ast.Call(
|
|
76
|
+
func=call.func,
|
|
77
|
+
args=[new_first_arg, *call.args[1:]],
|
|
78
|
+
keywords=[
|
|
79
|
+
*call.keywords,
|
|
80
|
+
ast.keyword(arg="_sa_data", value=sa_data),
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
stmt = ast.Expr(value=new_call)
|
|
84
|
+
ast.copy_location(stmt, donor)
|
|
85
|
+
ast.fix_missing_locations(stmt)
|
|
86
|
+
stmts.append(stmt)
|
|
87
|
+
return stmts
|
|
88
|
+
|
|
89
|
+
def visit_Expr(self, node: ast.Expr) -> ast.AST:
|
|
90
|
+
self.generic_visit(node)
|
|
91
|
+
call = node.value
|
|
92
|
+
if not (
|
|
93
|
+
isinstance(call, ast.Call)
|
|
94
|
+
and isinstance(call.func, ast.Attribute)
|
|
95
|
+
and call.func.attr == "soft_assert"
|
|
96
|
+
and call.args
|
|
97
|
+
and not any(kw.arg == "_sa_data" for kw in call.keywords)
|
|
98
|
+
):
|
|
99
|
+
return node
|
|
100
|
+
|
|
101
|
+
expr_arg = call.args[0]
|
|
102
|
+
source = ast.get_source_segment(self._source, expr_arg) or ""
|
|
103
|
+
n = ast.Name
|
|
104
|
+
|
|
105
|
+
self.changed = True
|
|
106
|
+
|
|
107
|
+
if isinstance(expr_arg, ast.Compare) and len(expr_arg.ops) == 1:
|
|
108
|
+
ln, rn = self._tmp(), self._tmp()
|
|
109
|
+
stmts = [
|
|
110
|
+
self._assign(ln, expr_arg.left, node),
|
|
111
|
+
self._assign(rn, expr_arg.comparators[0], node),
|
|
112
|
+
]
|
|
113
|
+
new_expr = ast.Compare(
|
|
114
|
+
left=n(id=ln, ctx=ast.Load()),
|
|
115
|
+
ops=list(expr_arg.ops),
|
|
116
|
+
comparators=[n(id=rn, ctx=ast.Load())],
|
|
117
|
+
)
|
|
118
|
+
op_str = _COMPARE_OPS.get(type(expr_arg.ops[0]), "??")
|
|
119
|
+
sa_data = ast.Tuple(
|
|
120
|
+
elts=[
|
|
121
|
+
ast.Constant(value="cmp"),
|
|
122
|
+
ast.Constant(value=source),
|
|
123
|
+
ast.Constant(value=op_str),
|
|
124
|
+
n(id=ln, ctx=ast.Load()),
|
|
125
|
+
n(id=rn, ctx=ast.Load()),
|
|
126
|
+
],
|
|
127
|
+
ctx=ast.Load(),
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
vn = self._tmp()
|
|
131
|
+
stmts = [self._assign(vn, expr_arg, node)]
|
|
132
|
+
new_expr = n(id=vn, ctx=ast.Load())
|
|
133
|
+
sa_data = ast.Tuple(
|
|
134
|
+
elts=[
|
|
135
|
+
ast.Constant(value="val"),
|
|
136
|
+
ast.Constant(value=source),
|
|
137
|
+
n(id=vn, ctx=ast.Load()),
|
|
138
|
+
],
|
|
139
|
+
ctx=ast.Load(),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return self._finish(stmts, call, new_expr, sa_data, node)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _SoftAssertLoader(importlib.abc.Loader):
|
|
146
|
+
"""Loader that compiles a module from AST-rewritten source."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, origin: str) -> None:
|
|
149
|
+
self._origin = origin
|
|
150
|
+
|
|
151
|
+
def create_module( # noqa: PLR6301
|
|
152
|
+
self,
|
|
153
|
+
spec: importlib.machinery.ModuleSpec,
|
|
154
|
+
) -> None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def exec_module(self, module: types.ModuleType) -> None:
|
|
158
|
+
module.__file__ = self._origin
|
|
159
|
+
source = Path(self._origin).read_text("utf-8")
|
|
160
|
+
|
|
161
|
+
tree = ast.parse(source, filename=self._origin)
|
|
162
|
+
transformer = SoftAssertTransformer(source)
|
|
163
|
+
tree = transformer.visit(tree)
|
|
164
|
+
|
|
165
|
+
if transformer.changed:
|
|
166
|
+
ast.fix_missing_locations(tree)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
from _pytest.assertion.rewrite import rewrite_asserts
|
|
170
|
+
|
|
171
|
+
rewrite_asserts(tree, source.encode("utf-8"))
|
|
172
|
+
except Exception: # noqa: BLE001
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
code = compile(tree, self._origin, "exec")
|
|
176
|
+
exec(code, module.__dict__) # noqa: S102
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class SoftAssertImportHook(importlib.abc.MetaPathFinder):
|
|
180
|
+
"""Import hook that rewrites ``soft_assert`` calls in test modules."""
|
|
181
|
+
|
|
182
|
+
def __init__(self) -> None:
|
|
183
|
+
self._finding: set[str] = set()
|
|
184
|
+
|
|
185
|
+
def find_spec(
|
|
186
|
+
self,
|
|
187
|
+
name: str,
|
|
188
|
+
path: object = None,
|
|
189
|
+
target: object = None,
|
|
190
|
+
) -> importlib.machinery.ModuleSpec | None:
|
|
191
|
+
if name in self._finding:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
parts = name.split(".")
|
|
195
|
+
last = parts[-1]
|
|
196
|
+
if not (last.startswith("test_") or last == "conftest"):
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
self._finding.add(name)
|
|
200
|
+
try:
|
|
201
|
+
spec = importlib.util.find_spec(name)
|
|
202
|
+
if not (
|
|
203
|
+
spec
|
|
204
|
+
and spec.origin
|
|
205
|
+
and spec.origin.endswith(".py")
|
|
206
|
+
and Path(spec.origin).exists()
|
|
207
|
+
):
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
source = Path(spec.origin).read_text("utf-8")
|
|
211
|
+
if "soft_assert" not in source:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
new_spec = importlib.machinery.ModuleSpec(
|
|
215
|
+
name,
|
|
216
|
+
_SoftAssertLoader(spec.origin),
|
|
217
|
+
origin=spec.origin,
|
|
218
|
+
is_package=spec.submodule_search_locations is not None,
|
|
219
|
+
)
|
|
220
|
+
new_spec.has_location = True
|
|
221
|
+
if spec.submodule_search_locations is not None:
|
|
222
|
+
new_spec.submodule_search_locations = list(
|
|
223
|
+
spec.submodule_search_locations,
|
|
224
|
+
)
|
|
225
|
+
return new_spec
|
|
226
|
+
except Exception: # noqa: BLE001
|
|
227
|
+
return None
|
|
228
|
+
finally:
|
|
229
|
+
self._finding.discard(name)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
_hook_instance: SoftAssertImportHook | None = None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def install() -> None:
|
|
236
|
+
"""Install the import hook at the front of ``sys.meta_path``."""
|
|
237
|
+
global _hook_instance # noqa: PLW0603
|
|
238
|
+
if _hook_instance is None:
|
|
239
|
+
_hook_instance = SoftAssertImportHook()
|
|
240
|
+
sys.meta_path.insert(0, _hook_instance)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def uninstall() -> None:
|
|
244
|
+
"""Remove the import hook from ``sys.meta_path``."""
|
|
245
|
+
global _hook_instance # noqa: PLW0603
|
|
246
|
+
if _hook_instance is not None:
|
|
247
|
+
with contextlib.suppress(ValueError):
|
|
248
|
+
sys.meta_path.remove(_hook_instance)
|
|
249
|
+
_hook_instance = None
|
|
@@ -11,7 +11,7 @@ import urllib3
|
|
|
11
11
|
from json_checker import Checker, CheckerError
|
|
12
12
|
from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver
|
|
13
13
|
|
|
14
|
-
from promium._introspect import _RESET, _WHITE,
|
|
14
|
+
from promium._introspect import _RESET, _WHITE, format_sa_data
|
|
15
15
|
from promium.common import upload_screenshot
|
|
16
16
|
|
|
17
17
|
|
|
@@ -161,10 +161,11 @@ class BaseSoftAssertion:
|
|
|
161
161
|
msg: str | None = None,
|
|
162
162
|
with_screen: bool = False,
|
|
163
163
|
hard_assert: bool | None = None,
|
|
164
|
+
_sa_data: tuple | None = None,
|
|
164
165
|
) -> None:
|
|
165
166
|
"""Check that the expression is true."""
|
|
166
167
|
if not expr:
|
|
167
|
-
source_expr =
|
|
168
|
+
source_expr = format_sa_data(_sa_data) if _sa_data else None
|
|
168
169
|
parts = []
|
|
169
170
|
if msg:
|
|
170
171
|
parts.append(f"{_WHITE}{msg}{_RESET}")
|
|
@@ -62,7 +62,7 @@ from promium.waits import (
|
|
|
62
62
|
log = logging.getLogger(__name__)
|
|
63
63
|
|
|
64
64
|
MAX_TRIES = 20
|
|
65
|
-
NORMAL_LOAD_TIME =
|
|
65
|
+
NORMAL_LOAD_TIME = 15
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
def with_retries(tries: int, second_to_wait: float) -> Any:
|
|
@@ -274,23 +274,23 @@ class Page:
|
|
|
274
274
|
"""Wait until page animations complete."""
|
|
275
275
|
wait_for_animation(self.driver)
|
|
276
276
|
|
|
277
|
-
def wait_document_status(self,
|
|
277
|
+
def wait_document_status(self, timeout: int = NORMAL_LOAD_TIME) -> None:
|
|
278
278
|
"""Wait until document readyState is complete."""
|
|
279
|
-
wait_document_status(self.driver,
|
|
279
|
+
wait_document_status(self.driver, timeout=timeout)
|
|
280
280
|
|
|
281
281
|
def wait_websocket_connection_open(
|
|
282
|
-
self,
|
|
282
|
+
self, timeout: int = NORMAL_LOAD_TIME
|
|
283
283
|
) -> None:
|
|
284
284
|
"""Wait until websocket connection is established."""
|
|
285
|
-
wait_websocket_connection_open(self.driver,
|
|
285
|
+
wait_websocket_connection_open(self.driver, timeout=timeout)
|
|
286
286
|
|
|
287
|
-
def wait_for_alert(self,
|
|
287
|
+
def wait_for_alert(self, timeout: int = NORMAL_LOAD_TIME) -> Alert:
|
|
288
288
|
"""Wait until an alert is present and return it."""
|
|
289
|
-
return wait_for_alert(self.driver,
|
|
289
|
+
return wait_for_alert(self.driver, timeout=timeout)
|
|
290
290
|
|
|
291
|
-
def is_alert_displayed(self,
|
|
291
|
+
def is_alert_displayed(self, timeout: int = NORMAL_LOAD_TIME) -> bool:
|
|
292
292
|
"""Return True if an alert is displayed within timeout."""
|
|
293
|
-
return wait_for_alert_is_displayed(self.driver,
|
|
293
|
+
return wait_for_alert_is_displayed(self.driver, timeout=timeout)
|
|
294
294
|
|
|
295
295
|
def switch_to_tab(self) -> None:
|
|
296
296
|
"""Switch to a newly opened browser tab."""
|
|
@@ -326,7 +326,7 @@ class Page:
|
|
|
326
326
|
scroll_into_end(self.driver)
|
|
327
327
|
|
|
328
328
|
def find_network_log(
|
|
329
|
-
self, find_mask: str, find_status: int = 200, timeout:
|
|
329
|
+
self, find_mask: str, find_status: int = 200, timeout: int = 5
|
|
330
330
|
) -> list:
|
|
331
331
|
"""Find and return network log entries matching given mask and status."""
|
|
332
332
|
return find_network_log(self.driver, find_mask, find_status, timeout)
|
|
@@ -606,7 +606,7 @@ class Collection(Base, Sequence):
|
|
|
606
606
|
index = random.randint(0, len(elements) - 1)
|
|
607
607
|
return self.get_item(index)
|
|
608
608
|
|
|
609
|
-
def wait_empty(self, timeout: float =
|
|
609
|
+
def wait_empty(self, timeout: float = NORMAL_LOAD_TIME) -> Self:
|
|
610
610
|
"""
|
|
611
611
|
Wait until the collection becomes empty.
|
|
612
612
|
|
|
@@ -643,7 +643,7 @@ class Collection(Base, Sequence):
|
|
|
643
643
|
)
|
|
644
644
|
raise ElementLocationException(digest) from e
|
|
645
645
|
|
|
646
|
-
def wait_not_empty(self, timeout: float =
|
|
646
|
+
def wait_not_empty(self, timeout: float = NORMAL_LOAD_TIME) -> Self:
|
|
647
647
|
"""
|
|
648
648
|
Wait until the collection becomes non-empty.
|
|
649
649
|
|
|
@@ -881,7 +881,9 @@ class ElementBase(Base):
|
|
|
881
881
|
}
|
|
882
882
|
return type(f"{cls.__name__}List", (Collection,), params)(by, locator)
|
|
883
883
|
|
|
884
|
-
def wait_to_display(
|
|
884
|
+
def wait_to_display(
|
|
885
|
+
self, timeout: int = NORMAL_LOAD_TIME, msg: str = ""
|
|
886
|
+
) -> Self:
|
|
885
887
|
"""
|
|
886
888
|
Wait until the element is visible.
|
|
887
889
|
|
|
@@ -924,7 +926,9 @@ class ElementBase(Base):
|
|
|
924
926
|
digest = f"{digest}\n{msg}"
|
|
925
927
|
raise ElementLocationException(digest) from None
|
|
926
928
|
|
|
927
|
-
def wait_to_present(
|
|
929
|
+
def wait_to_present(
|
|
930
|
+
self, timeout: int = NORMAL_LOAD_TIME, msg: str = ""
|
|
931
|
+
) -> Self:
|
|
928
932
|
"""
|
|
929
933
|
Wait until the element is present in DOM.
|
|
930
934
|
|
|
@@ -954,7 +958,9 @@ class ElementBase(Base):
|
|
|
954
958
|
digest = f"{digest}\n{msg}"
|
|
955
959
|
raise ElementLocationException(digest) from None
|
|
956
960
|
|
|
957
|
-
def wait_to_disappear(
|
|
961
|
+
def wait_to_disappear(
|
|
962
|
+
self, timeout: int = NORMAL_LOAD_TIME, msg: str = ""
|
|
963
|
+
) -> bool | None:
|
|
958
964
|
"""
|
|
959
965
|
Wait until element disappears from DOM or is hidden.
|
|
960
966
|
|
|
@@ -1002,12 +1008,12 @@ class ElementBase(Base):
|
|
|
1002
1008
|
digest = f"{digest}\n{msg}"
|
|
1003
1009
|
raise ElementLocationException(digest) from None
|
|
1004
1010
|
|
|
1005
|
-
def wait_in_viewport(self,
|
|
1011
|
+
def wait_in_viewport(self, timeout: int = NORMAL_LOAD_TIME) -> Self:
|
|
1006
1012
|
"""
|
|
1007
1013
|
Wait until the element is in the viewport.
|
|
1008
1014
|
|
|
1009
1015
|
Args:
|
|
1010
|
-
|
|
1016
|
+
timeout: Maximum wait time in seconds.
|
|
1011
1017
|
|
|
1012
1018
|
Raises:
|
|
1013
1019
|
ElementLocationException: If element is not in viewport.
|
|
@@ -1032,7 +1038,7 @@ class ElementBase(Base):
|
|
|
1032
1038
|
expression=lambda driver: driver.execute_script(
|
|
1033
1039
|
jquery_script, self.lookup()
|
|
1034
1040
|
),
|
|
1035
|
-
|
|
1041
|
+
timeout=timeout,
|
|
1036
1042
|
msg="",
|
|
1037
1043
|
)
|
|
1038
1044
|
return self
|
|
@@ -1042,7 +1048,7 @@ class ElementBase(Base):
|
|
|
1042
1048
|
title="Element is not in viewport",
|
|
1043
1049
|
locator=css,
|
|
1044
1050
|
condition="inViewport == true",
|
|
1045
|
-
timeout=
|
|
1051
|
+
timeout=timeout,
|
|
1046
1052
|
**self._digest_ctx(),
|
|
1047
1053
|
)
|
|
1048
1054
|
raise ElementLocationException(digest) from e
|
|
@@ -1349,7 +1355,6 @@ class Element(ElementBase):
|
|
|
1349
1355
|
return wait_until(
|
|
1350
1356
|
driver=self.driver,
|
|
1351
1357
|
expression=EC.staleness_of(self.lookup()),
|
|
1352
|
-
seconds=10,
|
|
1353
1358
|
)
|
|
1354
1359
|
except TimeoutException as e:
|
|
1355
1360
|
digest = build_digest(
|
|
@@ -1372,7 +1377,6 @@ class Element(ElementBase):
|
|
|
1372
1377
|
expression=EC.frame_to_be_available_and_switch_to_it(
|
|
1373
1378
|
self.lookup()
|
|
1374
1379
|
),
|
|
1375
|
-
seconds=10,
|
|
1376
1380
|
)
|
|
1377
1381
|
except TimeoutException as e:
|
|
1378
1382
|
digest = build_digest(
|
|
@@ -1397,7 +1401,6 @@ class Element(ElementBase):
|
|
|
1397
1401
|
try:
|
|
1398
1402
|
return wait_until(
|
|
1399
1403
|
driver=self.driver,
|
|
1400
|
-
seconds=10,
|
|
1401
1404
|
expression=text_to_be_present_in_element(self, text),
|
|
1402
1405
|
msg="",
|
|
1403
1406
|
)
|
|
@@ -1476,7 +1479,6 @@ class Element(ElementBase):
|
|
|
1476
1479
|
return wait_until(
|
|
1477
1480
|
driver=self.driver,
|
|
1478
1481
|
expression=lambda driver: text != self.get_attribute(name_attr),
|
|
1479
|
-
seconds=10,
|
|
1480
1482
|
msg="",
|
|
1481
1483
|
)
|
|
1482
1484
|
except TimeoutException as e:
|
|
@@ -1504,7 +1506,6 @@ class Element(ElementBase):
|
|
|
1504
1506
|
return wait_until(
|
|
1505
1507
|
driver=self.driver,
|
|
1506
1508
|
expression=lambda driver: element_text != self.text,
|
|
1507
|
-
seconds=5,
|
|
1508
1509
|
msg="",
|
|
1509
1510
|
)
|
|
1510
1511
|
except TimeoutException as e:
|
|
@@ -1518,18 +1519,18 @@ class Element(ElementBase):
|
|
|
1518
1519
|
)
|
|
1519
1520
|
raise ElementLocationException(digest) from e
|
|
1520
1521
|
|
|
1521
|
-
def wait_to_enabled(self,
|
|
1522
|
+
def wait_to_enabled(self, timeout: int = NORMAL_LOAD_TIME) -> Self:
|
|
1522
1523
|
"""
|
|
1523
1524
|
Wait until element is enabled (not disabled).
|
|
1524
1525
|
|
|
1525
1526
|
Args:
|
|
1526
|
-
|
|
1527
|
+
timeout: Maximum wait time in seconds.
|
|
1527
1528
|
"""
|
|
1528
1529
|
css = self._fmt_locator()
|
|
1529
|
-
push_step(f'wait_to_enabled("{css}", {
|
|
1530
|
+
push_step(f'wait_to_enabled("{css}", {timeout}s)')
|
|
1530
1531
|
try:
|
|
1531
1532
|
wait = WebDriverWait(
|
|
1532
|
-
self.driver,
|
|
1533
|
+
self.driver, timeout, ignored_exceptions=[WebDriverException]
|
|
1533
1534
|
)
|
|
1534
1535
|
return wait.until(
|
|
1535
1536
|
lambda driver: self
|
|
@@ -1542,7 +1543,7 @@ class Element(ElementBase):
|
|
|
1542
1543
|
title="Element is not enabled",
|
|
1543
1544
|
locator=css,
|
|
1544
1545
|
condition='attribute "disabled" != "true"',
|
|
1545
|
-
timeout=
|
|
1546
|
+
timeout=timeout,
|
|
1546
1547
|
**self._digest_ctx(),
|
|
1547
1548
|
)
|
|
1548
1549
|
raise ElementLocationException(digest) from e
|
|
@@ -473,6 +473,10 @@ def pytest_runtest_makereport(item: pytest.Item, call: CallInfo) -> None:
|
|
|
473
473
|
|
|
474
474
|
|
|
475
475
|
def pytest_configure(config: Config) -> None:
|
|
476
|
+
from promium._soft_assert_rewrite import install as _install_sa_hook
|
|
477
|
+
|
|
478
|
+
_install_sa_hook()
|
|
479
|
+
|
|
476
480
|
os.environ["PYTHONWARNINGS"] = (
|
|
477
481
|
"ignore:An HTTPS request has been made, "
|
|
478
482
|
"ignore:A true SSLContext object is not available, "
|
|
@@ -489,3 +493,9 @@ def pytest_configure(config: Config) -> None:
|
|
|
489
493
|
os.environ["CHECK_CONSOLE"] = "Enabled"
|
|
490
494
|
if not getattr(config, "lastfailed_file", None):
|
|
491
495
|
config.lastfailed_file = "lastfailed"
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def pytest_unconfigure(config: Config) -> None:
|
|
499
|
+
from promium._soft_assert_rewrite import uninstall as _uninstall_sa_hook
|
|
500
|
+
|
|
501
|
+
_uninstall_sa_hook()
|