13
13
# limitations under the License.
14
14
15
15
import collections .abc
16
- from typing import Any , List , Optional , Pattern , Sequence , Union
16
+ from types import TracebackType
17
+ from typing import (
18
+ TYPE_CHECKING ,
19
+ Any ,
20
+ Generic ,
21
+ List ,
22
+ Optional ,
23
+ Pattern ,
24
+ Sequence ,
25
+ Type ,
26
+ TypeVar ,
27
+ Union ,
28
+ )
17
29
from urllib .parse import urljoin
18
30
19
31
from playwright ._impl ._api_structures import (
29
41
from playwright ._impl ._page import Page
30
42
from playwright ._impl ._str_utils import escape_regex_flags
31
43
44
+ if TYPE_CHECKING :
45
+ from ..async_api import Expect as AsyncExpect
46
+ from ..sync_api import Expect as SyncExpect
47
+
32
48
33
49
class AssertionsBase :
34
50
def __init__ (
@@ -37,13 +53,15 @@ def __init__(
37
53
timeout : float = None ,
38
54
is_not : bool = False ,
39
55
message : Optional [str ] = None ,
56
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
40
57
) -> None :
41
58
self ._actual_locator = locator
42
59
self ._loop = locator ._loop
43
60
self ._dispatcher_fiber = locator ._dispatcher_fiber
44
61
self ._timeout = timeout
45
62
self ._is_not = is_not
46
63
self ._custom_message = message
64
+ self ._soft_context = soft_context
47
65
48
66
async def _expect_impl (
49
67
self ,
@@ -71,9 +89,13 @@ async def _expect_impl(
71
89
out_message = (
72
90
f"{ message } '{ expected } '" if expected is not None else f"{ message } "
73
91
)
74
- raise AssertionError (
92
+ error = AssertionError (
75
93
f"{ out_message } \n Actual value: { actual } { format_call_log (result .get ('log' ))} "
76
94
)
95
+ if self ._soft_context is not None :
96
+ self ._soft_context .add_failure (error )
97
+ else :
98
+ raise error
77
99
78
100
79
101
class PageAssertions (AssertionsBase ):
@@ -83,14 +105,19 @@ def __init__(
83
105
timeout : float = None ,
84
106
is_not : bool = False ,
85
107
message : Optional [str ] = None ,
108
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
86
109
) -> None :
87
- super ().__init__ (page .locator (":root" ), timeout , is_not , message )
110
+ super ().__init__ (page .locator (":root" ), timeout , is_not , message , soft_context )
88
111
self ._actual_page = page
89
112
90
113
@property
91
114
def _not (self ) -> "PageAssertions" :
92
115
return PageAssertions (
93
- self ._actual_page , self ._timeout , not self ._is_not , self ._custom_message
116
+ self ._actual_page ,
117
+ self ._timeout ,
118
+ not self ._is_not ,
119
+ self ._custom_message ,
120
+ self ._soft_context ,
94
121
)
95
122
96
123
async def to_have_title (
@@ -148,14 +175,19 @@ def __init__(
148
175
timeout : float = None ,
149
176
is_not : bool = False ,
150
177
message : Optional [str ] = None ,
178
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
151
179
) -> None :
152
- super ().__init__ (locator , timeout , is_not , message )
180
+ super ().__init__ (locator , timeout , is_not , message , soft_context )
153
181
self ._actual_locator = locator
154
182
155
183
@property
156
184
def _not (self ) -> "LocatorAssertions" :
157
185
return LocatorAssertions (
158
- self ._actual_locator , self ._timeout , not self ._is_not , self ._custom_message
186
+ self ._actual_locator ,
187
+ self ._timeout ,
188
+ not self ._is_not ,
189
+ self ._custom_message ,
190
+ self ._soft_context ,
159
191
)
160
192
161
193
async def to_contain_text (
@@ -848,18 +880,24 @@ def __init__(
848
880
timeout : float = None ,
849
881
is_not : bool = False ,
850
882
message : Optional [str ] = None ,
883
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
851
884
) -> None :
852
885
self ._loop = response ._loop
853
886
self ._dispatcher_fiber = response ._dispatcher_fiber
854
887
self ._timeout = timeout
855
888
self ._is_not = is_not
856
889
self ._actual = response
857
890
self ._custom_message = message
891
+ self ._soft_context = soft_context
858
892
859
893
@property
860
894
def _not (self ) -> "APIResponseAssertions" :
861
895
return APIResponseAssertions (
862
- self ._actual , self ._timeout , not self ._is_not , self ._custom_message
896
+ self ._actual ,
897
+ self ._timeout ,
898
+ not self ._is_not ,
899
+ self ._custom_message ,
900
+ self ._soft_context ,
863
901
)
864
902
865
903
async def to_be_ok (
@@ -880,7 +918,11 @@ async def to_be_ok(
880
918
if text is not None :
881
919
out_message += f"\n Response Text:\n { text [:1000 ]} "
882
920
883
- raise AssertionError (out_message )
921
+ error = AssertionError (out_message )
922
+ if self ._soft_context is not None :
923
+ self ._soft_context .add_failure (error )
924
+ else :
925
+ raise error
884
926
885
927
async def not_to_be_ok (self ) -> None :
886
928
__tracebackhide__ = True
@@ -933,3 +975,58 @@ def to_expected_text_values(
933
975
else :
934
976
raise Error ("value must be a string or regular expression" )
935
977
return out
978
+
979
+
980
+ class SoftAssertionContext :
981
+ def __init__ (self ) -> None :
982
+ self ._failures : List [Exception ] = []
983
+
984
+ def __repr__ (self ) -> str :
985
+ return f"<SoftAssertionContext failures={ self ._failures !r} >"
986
+
987
+ def add_failure (self , error : Exception ) -> None :
988
+ self ._failures .append (error )
989
+
990
+ def has_failures (self ) -> bool :
991
+ return bool (self ._failures )
992
+
993
+ def get_failure_messages (self ) -> str :
994
+ return "\n " .join (
995
+ f"{ i } . { str (error )} " for i , error in enumerate (self ._failures , 1 )
996
+ )
997
+
998
+
999
+ E = TypeVar ("E" , "SyncExpect" , "AsyncExpect" )
1000
+
1001
+
1002
+ class SoftAssertionContextManager (Generic [E ]):
1003
+ def __init__ (self , expect : E , context : SoftAssertionContext ) -> None :
1004
+ self ._expect : E = expect
1005
+ self ._context = context
1006
+
1007
+ def __enter__ (self ) -> E :
1008
+ self ._expect ._soft_context = self ._context
1009
+ return self ._expect
1010
+
1011
+ def __exit__ (
1012
+ self ,
1013
+ exc_type : Optional [Type [BaseException ]],
1014
+ exc_val : Optional [BaseException ],
1015
+ exc_tb : Optional [TracebackType ],
1016
+ ) -> None :
1017
+ __tracebackhide__ = True
1018
+
1019
+ if self ._context .has_failures ():
1020
+ if exc_type is not None :
1021
+ failure_message = (
1022
+ f"{ str (exc_val )} "
1023
+ f"\n \n The above exception occurred within soft assertion block."
1024
+ f"\n \n Soft assertion failures:\n { self ._context .get_failure_messages ()} "
1025
+ )
1026
+ if exc_val is not None :
1027
+ exc_val .args = (failure_message ,) + exc_val .args [1 :]
1028
+ return
1029
+
1030
+ raise AssertionError (
1031
+ f"Soft assertion failures\n { self ._context .get_failure_messages ()} "
1032
+ )
0 commit comments