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 (
2941from playwright ._impl ._page import Page
3042from playwright ._impl ._str_utils import escape_regex_flags
3143
44+ if TYPE_CHECKING :
45+ from ..async_api import Expect as AsyncExpect
46+ from ..sync_api import Expect as SyncExpect
47+
3248
3349class AssertionsBase :
3450 def __init__ (
@@ -37,13 +53,15 @@ def __init__(
3753 timeout : float = None ,
3854 is_not : bool = False ,
3955 message : Optional [str ] = None ,
56+ soft_context : Optional ["SoftAssertionContext" ] = None ,
4057 ) -> None :
4158 self ._actual_locator = locator
4259 self ._loop = locator ._loop
4360 self ._dispatcher_fiber = locator ._dispatcher_fiber
4461 self ._timeout = timeout
4562 self ._is_not = is_not
4663 self ._custom_message = message
64+ self ._soft_context = soft_context
4765
4866 async def _expect_impl (
4967 self ,
@@ -71,9 +89,13 @@ async def _expect_impl(
7189 out_message = (
7290 f"{ message } '{ expected } '" if expected is not None else f"{ message } "
7391 )
74- raise AssertionError (
92+ error = AssertionError (
7593 f"{ out_message } \n Actual value: { actual } { format_call_log (result .get ('log' ))} "
7694 )
95+ if self ._soft_context is not None :
96+ self ._soft_context .add_failure (error )
97+ else :
98+ raise error
7799
78100
79101class PageAssertions (AssertionsBase ):
@@ -83,14 +105,19 @@ def __init__(
83105 timeout : float = None ,
84106 is_not : bool = False ,
85107 message : Optional [str ] = None ,
108+ soft_context : Optional ["SoftAssertionContext" ] = None ,
86109 ) -> None :
87- super ().__init__ (page .locator (":root" ), timeout , is_not , message )
110+ super ().__init__ (page .locator (":root" ), timeout , is_not , message , soft_context )
88111 self ._actual_page = page
89112
90113 @property
91114 def _not (self ) -> "PageAssertions" :
92115 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 ,
94121 )
95122
96123 async def to_have_title (
@@ -148,14 +175,19 @@ def __init__(
148175 timeout : float = None ,
149176 is_not : bool = False ,
150177 message : Optional [str ] = None ,
178+ soft_context : Optional ["SoftAssertionContext" ] = None ,
151179 ) -> None :
152- super ().__init__ (locator , timeout , is_not , message )
180+ super ().__init__ (locator , timeout , is_not , message , soft_context )
153181 self ._actual_locator = locator
154182
155183 @property
156184 def _not (self ) -> "LocatorAssertions" :
157185 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 ,
159191 )
160192
161193 async def to_contain_text (
@@ -848,18 +880,24 @@ def __init__(
848880 timeout : float = None ,
849881 is_not : bool = False ,
850882 message : Optional [str ] = None ,
883+ soft_context : Optional ["SoftAssertionContext" ] = None ,
851884 ) -> None :
852885 self ._loop = response ._loop
853886 self ._dispatcher_fiber = response ._dispatcher_fiber
854887 self ._timeout = timeout
855888 self ._is_not = is_not
856889 self ._actual = response
857890 self ._custom_message = message
891+ self ._soft_context = soft_context
858892
859893 @property
860894 def _not (self ) -> "APIResponseAssertions" :
861895 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 ,
863901 )
864902
865903 async def to_be_ok (
@@ -880,7 +918,11 @@ async def to_be_ok(
880918 if text is not None :
881919 out_message += f"\n Response Text:\n { text [:1000 ]} "
882920
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
884926
885927 async def not_to_be_ok (self ) -> None :
886928 __tracebackhide__ = True
@@ -933,3 +975,58 @@ def to_expected_text_values(
933975 else :
934976 raise Error ("value must be a string or regular expression" )
935977 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