1313# limitations under the License.
1414
1515import collections .abc
16- from typing import Any , List , Optional , Pattern , Sequence , Union
16+ from contextlib import contextmanager
17+ from typing import Any , Iterator , List , Optional , Pattern , Sequence , Union
1718from urllib .parse import urljoin
1819
1920from playwright ._impl ._api_structures import (
3031from playwright ._impl ._page import Page
3132from playwright ._impl ._str_utils import escape_regex_flags
3233
34+ _soft_errors : Optional [List [AssertionError ]] = None
35+
36+
37+ @contextmanager
38+ def _soft_scope () -> Iterator [List [AssertionError ]]:
39+ global _soft_errors
40+ assert _soft_errors is None , "nested soft assertion scopes are not supported"
41+ _soft_errors = []
42+ try :
43+ yield _soft_errors
44+ finally :
45+ _soft_errors = None
46+
47+
48+ def _record_soft_or_raise (error : AssertionError , is_soft : bool ) -> None :
49+ __tracebackhide__ = True
50+ if is_soft :
51+ if _soft_errors is None :
52+ raise RuntimeError (
53+ "expect.soft(...) requires pytest-playwright>=0.7.3 "
54+ "(or pytest-playwright-asyncio>=0.7.3). Upgrade the plugin, "
55+ "or use a regular expect(...) assertion."
56+ )
57+ _soft_errors .append (error )
58+ return
59+ raise error
60+
3361
3462class AssertionsBase :
3563 def __init__ (
@@ -38,13 +66,15 @@ def __init__(
3866 timeout : float = None ,
3967 is_not : bool = False ,
4068 message : Optional [str ] = None ,
69+ is_soft : bool = False ,
4170 ) -> None :
4271 self ._actual_locator = locator
4372 self ._loop = locator ._loop
4473 self ._dispatcher_fiber = locator ._dispatcher_fiber
4574 self ._timeout = timeout
4675 self ._is_not = is_not
4776 self ._custom_message = message
77+ self ._is_soft = is_soft
4878
4979 async def _call_expect (
5080 self , expression : str , expect_options : FrameExpectOptions , title : Optional [str ]
@@ -82,8 +112,11 @@ async def _expect_impl(
82112 )
83113 error_message = result .get ("errorMessage" )
84114 error_message = f"\n { error_message } " if error_message else ""
85- raise AssertionError (
86- f"{ out_message } \n Actual value: { actual } { error_message } { format_call_log (result .get ('log' ))} "
115+ _record_soft_or_raise (
116+ AssertionError (
117+ f"{ out_message } \n Actual value: { actual } { error_message } { format_call_log (result .get ('log' ))} "
118+ ),
119+ self ._is_soft ,
87120 )
88121
89122
@@ -94,8 +127,9 @@ def __init__(
94127 timeout : float = None ,
95128 is_not : bool = False ,
96129 message : Optional [str ] = None ,
130+ is_soft : bool = False ,
97131 ) -> None :
98- super ().__init__ (page .locator (":root" ), timeout , is_not , message )
132+ super ().__init__ (page .locator (":root" ), timeout , is_not , message , is_soft )
99133 self ._actual_page = page
100134
101135 async def _call_expect (
@@ -109,7 +143,11 @@ async def _call_expect(
109143 @property
110144 def _not (self ) -> "PageAssertions" :
111145 return PageAssertions (
112- self ._actual_page , self ._timeout , not self ._is_not , self ._custom_message
146+ self ._actual_page ,
147+ self ._timeout ,
148+ not self ._is_not ,
149+ self ._custom_message ,
150+ self ._is_soft ,
113151 )
114152
115153 async def to_have_title (
@@ -169,8 +207,9 @@ def __init__(
169207 timeout : float = None ,
170208 is_not : bool = False ,
171209 message : Optional [str ] = None ,
210+ is_soft : bool = False ,
172211 ) -> None :
173- super ().__init__ (locator , timeout , is_not , message )
212+ super ().__init__ (locator , timeout , is_not , message , is_soft )
174213 self ._actual_locator = locator
175214
176215 async def _call_expect (
@@ -182,7 +221,11 @@ async def _call_expect(
182221 @property
183222 def _not (self ) -> "LocatorAssertions" :
184223 return LocatorAssertions (
185- self ._actual_locator , self ._timeout , not self ._is_not , self ._custom_message
224+ self ._actual_locator ,
225+ self ._timeout ,
226+ not self ._is_not ,
227+ self ._custom_message ,
228+ self ._is_soft ,
186229 )
187230
188231 async def to_contain_text (
@@ -944,18 +987,24 @@ def __init__(
944987 timeout : float = None ,
945988 is_not : bool = False ,
946989 message : Optional [str ] = None ,
990+ is_soft : bool = False ,
947991 ) -> None :
948992 self ._loop = response ._loop
949993 self ._dispatcher_fiber = response ._dispatcher_fiber
950994 self ._timeout = timeout
951995 self ._is_not = is_not
952996 self ._actual = response
953997 self ._custom_message = message
998+ self ._is_soft = is_soft
954999
9551000 @property
9561001 def _not (self ) -> "APIResponseAssertions" :
9571002 return APIResponseAssertions (
958- self ._actual , self ._timeout , not self ._is_not , self ._custom_message
1003+ self ._actual ,
1004+ self ._timeout ,
1005+ not self ._is_not ,
1006+ self ._custom_message ,
1007+ self ._is_soft ,
9591008 )
9601009
9611010 async def to_be_ok (
@@ -976,7 +1025,7 @@ async def to_be_ok(
9761025 if text is not None :
9771026 out_message += f"\n Response Text:\n { text [:1000 ]} "
9781027
979- raise AssertionError (out_message )
1028+ _record_soft_or_raise ( AssertionError (out_message ), self . _is_soft )
9801029
9811030 async def not_to_be_ok (self ) -> None :
9821031 __tracebackhide__ = True
0 commit comments