1313# limitations under the License.
1414
1515import 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+ )
1729from urllib .parse import urljoin
1830
1931from playwright ._impl ._api_structures import (
3042from playwright ._impl ._page import Page
3143from playwright ._impl ._str_utils import escape_regex_flags
3244
45+ if TYPE_CHECKING :
46+ from ..async_api import Expect as AsyncExpect
47+ from ..sync_api import Expect as SyncExpect
48+
3349
3450class AssertionsBase :
3551 def __init__ (
@@ -38,13 +54,15 @@ def __init__(
3854 timeout : float = None ,
3955 is_not : bool = False ,
4056 message : Optional [str ] = None ,
57+ soft_context : Optional ["SoftAssertionContext" ] = None ,
4158 ) -> None :
4259 self ._actual_locator = locator
4360 self ._loop = locator ._loop
4461 self ._dispatcher_fiber = locator ._dispatcher_fiber
4562 self ._timeout = timeout
4663 self ._is_not = is_not
4764 self ._custom_message = message
65+ self ._soft_context = soft_context
4866
4967 async def _call_expect (
5068 self , expression : str , expect_options : FrameExpectOptions , title : Optional [str ]
@@ -80,9 +98,13 @@ async def _expect_impl(
8098 out_message = (
8199 f"{ message } '{ expected } '" if expected is not None else f"{ message } "
82100 )
83- raise AssertionError (
101+ error = AssertionError (
84102 f"{ out_message } \n Actual value: { actual } { format_call_log (result .get ('log' ))} "
85103 )
104+ if self ._soft_context is not None :
105+ self ._soft_context .add_failure (error )
106+ else :
107+ raise error
86108
87109
88110class PageAssertions (AssertionsBase ):
@@ -92,8 +114,9 @@ def __init__(
92114 timeout : float = None ,
93115 is_not : bool = False ,
94116 message : Optional [str ] = None ,
117+ soft_context : Optional ["SoftAssertionContext" ] = None ,
95118 ) -> None :
96- super ().__init__ (page .locator (":root" ), timeout , is_not , message )
119+ super ().__init__ (page .locator (":root" ), timeout , is_not , message , soft_context )
97120 self ._actual_page = page
98121
99122 async def _call_expect (
@@ -107,7 +130,11 @@ async def _call_expect(
107130 @property
108131 def _not (self ) -> "PageAssertions" :
109132 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 ,
111138 )
112139
113140 async def to_have_title (
@@ -167,8 +194,9 @@ def __init__(
167194 timeout : float = None ,
168195 is_not : bool = False ,
169196 message : Optional [str ] = None ,
197+ soft_context : Optional ["SoftAssertionContext" ] = None ,
170198 ) -> None :
171- super ().__init__ (locator , timeout , is_not , message )
199+ super ().__init__ (locator , timeout , is_not , message , soft_context )
172200 self ._actual_locator = locator
173201
174202 async def _call_expect (
@@ -180,7 +208,11 @@ async def _call_expect(
180208 @property
181209 def _not (self ) -> "LocatorAssertions" :
182210 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 ,
184216 )
185217
186218 async def to_contain_text (
@@ -942,18 +974,24 @@ def __init__(
942974 timeout : float = None ,
943975 is_not : bool = False ,
944976 message : Optional [str ] = None ,
977+ soft_context : Optional ["SoftAssertionContext" ] = None ,
945978 ) -> None :
946979 self ._loop = response ._loop
947980 self ._dispatcher_fiber = response ._dispatcher_fiber
948981 self ._timeout = timeout
949982 self ._is_not = is_not
950983 self ._actual = response
951984 self ._custom_message = message
985+ self ._soft_context = soft_context
952986
953987 @property
954988 def _not (self ) -> "APIResponseAssertions" :
955989 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 ,
957995 )
958996
959997 async def to_be_ok (
@@ -974,7 +1012,11 @@ async def to_be_ok(
9741012 if text is not None :
9751013 out_message += f"\n Response Text:\n { text [:1000 ]} "
9761014
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
9781020
9791021 async def not_to_be_ok (self ) -> None :
9801022 __tracebackhide__ = True
@@ -1027,3 +1069,58 @@ def to_expected_text_values(
10271069 else :
10281070 raise Error ("value must be a string or regular expression" )
10291071 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