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 (
30
42
from playwright ._impl ._page import Page
31
43
from playwright ._impl ._str_utils import escape_regex_flags
32
44
45
+ if TYPE_CHECKING :
46
+ from ..async_api import Expect as AsyncExpect
47
+ from ..sync_api import Expect as SyncExpect
48
+
33
49
34
50
class AssertionsBase :
35
51
def __init__ (
@@ -38,13 +54,15 @@ def __init__(
38
54
timeout : float = None ,
39
55
is_not : bool = False ,
40
56
message : Optional [str ] = None ,
57
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
41
58
) -> None :
42
59
self ._actual_locator = locator
43
60
self ._loop = locator ._loop
44
61
self ._dispatcher_fiber = locator ._dispatcher_fiber
45
62
self ._timeout = timeout
46
63
self ._is_not = is_not
47
64
self ._custom_message = message
65
+ self ._soft_context = soft_context
48
66
49
67
async def _call_expect (
50
68
self , expression : str , expect_options : FrameExpectOptions , title : Optional [str ]
@@ -80,9 +98,13 @@ async def _expect_impl(
80
98
out_message = (
81
99
f"{ message } '{ expected } '" if expected is not None else f"{ message } "
82
100
)
83
- raise AssertionError (
101
+ error = AssertionError (
84
102
f"{ out_message } \n Actual value: { actual } { format_call_log (result .get ('log' ))} "
85
103
)
104
+ if self ._soft_context is not None :
105
+ self ._soft_context .add_failure (error )
106
+ else :
107
+ raise error
86
108
87
109
88
110
class PageAssertions (AssertionsBase ):
@@ -92,8 +114,9 @@ def __init__(
92
114
timeout : float = None ,
93
115
is_not : bool = False ,
94
116
message : Optional [str ] = None ,
117
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
95
118
) -> None :
96
- super ().__init__ (page .locator (":root" ), timeout , is_not , message )
119
+ super ().__init__ (page .locator (":root" ), timeout , is_not , message , soft_context )
97
120
self ._actual_page = page
98
121
99
122
async def _call_expect (
@@ -107,7 +130,11 @@ async def _call_expect(
107
130
@property
108
131
def _not (self ) -> "PageAssertions" :
109
132
return PageAssertions (
110
- self ._actual_page , self ._timeout , not self ._is_not , self ._custom_message
133
+ self ._actual_page ,
134
+ self ._timeout ,
135
+ not self ._is_not ,
136
+ self ._custom_message ,
137
+ self ._soft_context ,
111
138
)
112
139
113
140
async def to_have_title (
@@ -167,8 +194,9 @@ def __init__(
167
194
timeout : float = None ,
168
195
is_not : bool = False ,
169
196
message : Optional [str ] = None ,
197
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
170
198
) -> None :
171
- super ().__init__ (locator , timeout , is_not , message )
199
+ super ().__init__ (locator , timeout , is_not , message , soft_context )
172
200
self ._actual_locator = locator
173
201
174
202
async def _call_expect (
@@ -180,7 +208,11 @@ async def _call_expect(
180
208
@property
181
209
def _not (self ) -> "LocatorAssertions" :
182
210
return LocatorAssertions (
183
- self ._actual_locator , self ._timeout , not self ._is_not , self ._custom_message
211
+ self ._actual_locator ,
212
+ self ._timeout ,
213
+ not self ._is_not ,
214
+ self ._custom_message ,
215
+ self ._soft_context ,
184
216
)
185
217
186
218
async def to_contain_text (
@@ -942,18 +974,24 @@ def __init__(
942
974
timeout : float = None ,
943
975
is_not : bool = False ,
944
976
message : Optional [str ] = None ,
977
+ soft_context : Optional ["SoftAssertionContext" ] = None ,
945
978
) -> None :
946
979
self ._loop = response ._loop
947
980
self ._dispatcher_fiber = response ._dispatcher_fiber
948
981
self ._timeout = timeout
949
982
self ._is_not = is_not
950
983
self ._actual = response
951
984
self ._custom_message = message
985
+ self ._soft_context = soft_context
952
986
953
987
@property
954
988
def _not (self ) -> "APIResponseAssertions" :
955
989
return APIResponseAssertions (
956
- self ._actual , self ._timeout , not self ._is_not , self ._custom_message
990
+ self ._actual ,
991
+ self ._timeout ,
992
+ not self ._is_not ,
993
+ self ._custom_message ,
994
+ self ._soft_context ,
957
995
)
958
996
959
997
async def to_be_ok (
@@ -974,7 +1012,11 @@ async def to_be_ok(
974
1012
if text is not None :
975
1013
out_message += f"\n Response Text:\n { text [:1000 ]} "
976
1014
977
- raise AssertionError (out_message )
1015
+ error = AssertionError (out_message )
1016
+ if self ._soft_context is not None :
1017
+ self ._soft_context .add_failure (error )
1018
+ else :
1019
+ raise error
978
1020
979
1021
async def not_to_be_ok (self ) -> None :
980
1022
__tracebackhide__ = True
@@ -1027,3 +1069,58 @@ def to_expected_text_values(
1027
1069
else :
1028
1070
raise Error ("value must be a string or regular expression" )
1029
1071
return out
1072
+
1073
+
1074
+ class SoftAssertionContext :
1075
+ def __init__ (self ) -> None :
1076
+ self ._failures : List [Exception ] = []
1077
+
1078
+ def __repr__ (self ) -> str :
1079
+ return f"<SoftAssertionContext failures={ self ._failures !r} >"
1080
+
1081
+ def add_failure (self , error : Exception ) -> None :
1082
+ self ._failures .append (error )
1083
+
1084
+ def has_failures (self ) -> bool :
1085
+ return bool (self ._failures )
1086
+
1087
+ def get_failure_messages (self ) -> str :
1088
+ return "\n " .join (
1089
+ f"{ i } . { str (error )} " for i , error in enumerate (self ._failures , 1 )
1090
+ )
1091
+
1092
+
1093
+ E = TypeVar ("E" , "SyncExpect" , "AsyncExpect" )
1094
+
1095
+
1096
+ class SoftAssertionContextManager (Generic [E ]):
1097
+ def __init__ (self , expect : E , context : SoftAssertionContext ) -> None :
1098
+ self ._expect : E = expect
1099
+ self ._context = context
1100
+
1101
+ def __enter__ (self ) -> E :
1102
+ self ._expect ._soft_context = self ._context
1103
+ return self ._expect
1104
+
1105
+ def __exit__ (
1106
+ self ,
1107
+ exc_type : Optional [Type [BaseException ]],
1108
+ exc_val : Optional [BaseException ],
1109
+ exc_tb : Optional [TracebackType ],
1110
+ ) -> None :
1111
+ __tracebackhide__ = True
1112
+
1113
+ if self ._context .has_failures ():
1114
+ if exc_type is not None :
1115
+ failure_message = (
1116
+ f"{ str (exc_val )} "
1117
+ f"\n \n The above exception occurred within soft assertion block."
1118
+ f"\n \n Soft assertion failures:\n { self ._context .get_failure_messages ()} "
1119
+ )
1120
+ if exc_val is not None :
1121
+ exc_val .args = (failure_message ,) + exc_val .args [1 :]
1122
+ return
1123
+
1124
+ raise AssertionError (
1125
+ f"Soft assertion failures\n { self ._context .get_failure_messages ()} "
1126
+ )
0 commit comments