1313# limitations under the License.
1414
1515import collections .abc
16- from typing import Any , List , Literal , Optional , Pattern , Sequence , Union
16+ from contextlib import contextmanager
17+ from typing import Any , Iterator , List , Literal , Optional , Pattern , Sequence , Union
1718from urllib .parse import urljoin
1819
1920from playwright ._impl ._api_structures import (
3132from playwright ._impl ._page import Page
3233from playwright ._impl ._str_utils import escape_regex_flags
3334
35+ _soft_errors : Optional [List [AssertionError ]] = None
36+
37+
38+ @contextmanager
39+ def _soft_scope () -> Iterator [List [AssertionError ]]:
40+ global _soft_errors
41+ assert _soft_errors is None , "nested soft assertion scopes are not supported"
42+ _soft_errors = []
43+ try :
44+ yield _soft_errors
45+ finally :
46+ _soft_errors = None
47+
48+
49+ def _record_soft_or_raise (error : AssertionError , is_soft : bool ) -> None :
50+ __tracebackhide__ = True
51+ if is_soft :
52+ if _soft_errors is None :
53+ raise RuntimeError (
54+ "expect.soft(...) requires pytest-playwright>=0.7.3 "
55+ "(or pytest-playwright-asyncio>=0.7.3). Upgrade the plugin, "
56+ "or use a regular expect(...) assertion."
57+ )
58+ _soft_errors .append (error )
59+ return
60+ raise error
61+
3462
3563class AssertionsBase :
3664 def __init__ (
@@ -39,13 +67,15 @@ def __init__(
3967 timeout : float = None ,
4068 is_not : bool = False ,
4169 message : Optional [str ] = None ,
70+ is_soft : bool = False ,
4271 ) -> None :
4372 self ._actual_locator = locator
4473 self ._loop = locator ._loop
4574 self ._dispatcher_fiber = locator ._dispatcher_fiber
4675 self ._timeout = timeout
4776 self ._is_not = is_not
4877 self ._custom_message = message
78+ self ._is_soft = is_soft
4979
5080 async def _call_expect (
5181 self , expression : str , expect_options : FrameExpectOptions , title : Optional [str ]
@@ -90,8 +120,11 @@ async def _expect_impl(
90120 )
91121 error_message = result .get ("errorMessage" )
92122 error_message = f"\n { error_message } " if error_message else ""
93- raise AssertionError (
94- f"{ out_message } \n Actual value: { actual } { error_message } { format_call_log (result .get ('log' ))} "
123+ _record_soft_or_raise (
124+ AssertionError (
125+ f"{ out_message } \n Actual value: { actual } { error_message } { format_call_log (result .get ('log' ))} "
126+ ),
127+ self ._is_soft ,
95128 )
96129
97130
@@ -102,8 +135,9 @@ def __init__(
102135 timeout : float = None ,
103136 is_not : bool = False ,
104137 message : Optional [str ] = None ,
138+ is_soft : bool = False ,
105139 ) -> None :
106- super ().__init__ (page .locator (":root" ), timeout , is_not , message )
140+ super ().__init__ (page .locator (":root" ), timeout , is_not , message , is_soft )
107141 self ._actual_page = page
108142
109143 async def _call_expect (
@@ -117,7 +151,11 @@ async def _call_expect(
117151 @property
118152 def _not (self ) -> "PageAssertions" :
119153 return PageAssertions (
120- self ._actual_page , self ._timeout , not self ._is_not , self ._custom_message
154+ self ._actual_page ,
155+ self ._timeout ,
156+ not self ._is_not ,
157+ self ._custom_message ,
158+ self ._is_soft ,
121159 )
122160
123161 async def to_have_title (
@@ -195,8 +233,9 @@ def __init__(
195233 timeout : float = None ,
196234 is_not : bool = False ,
197235 message : Optional [str ] = None ,
236+ is_soft : bool = False ,
198237 ) -> None :
199- super ().__init__ (locator , timeout , is_not , message )
238+ super ().__init__ (locator , timeout , is_not , message , is_soft )
200239 self ._actual_locator = locator
201240
202241 async def _call_expect (
@@ -208,7 +247,11 @@ async def _call_expect(
208247 @property
209248 def _not (self ) -> "LocatorAssertions" :
210249 return LocatorAssertions (
211- self ._actual_locator , self ._timeout , not self ._is_not , self ._custom_message
250+ self ._actual_locator ,
251+ self ._timeout ,
252+ not self ._is_not ,
253+ self ._custom_message ,
254+ self ._is_soft ,
212255 )
213256
214257 async def to_contain_text (
@@ -974,18 +1017,24 @@ def __init__(
9741017 timeout : float = None ,
9751018 is_not : bool = False ,
9761019 message : Optional [str ] = None ,
1020+ is_soft : bool = False ,
9771021 ) -> None :
9781022 self ._loop = response ._loop
9791023 self ._dispatcher_fiber = response ._dispatcher_fiber
9801024 self ._timeout = timeout
9811025 self ._is_not = is_not
9821026 self ._actual = response
9831027 self ._custom_message = message
1028+ self ._is_soft = is_soft
9841029
9851030 @property
9861031 def _not (self ) -> "APIResponseAssertions" :
9871032 return APIResponseAssertions (
988- self ._actual , self ._timeout , not self ._is_not , self ._custom_message
1033+ self ._actual ,
1034+ self ._timeout ,
1035+ not self ._is_not ,
1036+ self ._custom_message ,
1037+ self ._is_soft ,
9891038 )
9901039
9911040 async def to_be_ok (
@@ -1006,7 +1055,7 @@ async def to_be_ok(
10061055 if text is not None :
10071056 out_message += f"\n Response Text:\n { text [:1000 ]} "
10081057
1009- raise AssertionError (out_message )
1058+ _record_soft_or_raise ( AssertionError (out_message ), self . _is_soft )
10101059
10111060 async def not_to_be_ok (self ) -> None :
10121061 __tracebackhide__ = True
0 commit comments