Skip to content

Commit 177855e

Browse files
authored
chore: add ability to set expect timeout (microsoft#1918)
1 parent 4394ede commit 177855e

File tree

5 files changed

+154
-68
lines changed

5 files changed

+154
-68
lines changed

playwright/_impl/_assertions.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@
2525

2626
class AssertionsBase:
2727
def __init__(
28-
self, locator: Locator, is_not: bool = False, message: Optional[str] = None
28+
self,
29+
locator: Locator,
30+
timeout: float = None,
31+
is_not: bool = False,
32+
message: Optional[str] = None,
2933
) -> None:
3034
self._actual_locator = locator
3135
self._loop = locator._loop
3236
self._dispatcher_fiber = locator._dispatcher_fiber
37+
self._timeout = timeout
3338
self._is_not = is_not
3439
self._custom_message = message
3540

@@ -43,7 +48,7 @@ async def _expect_impl(
4348
__tracebackhide__ = True
4449
expect_options["isNot"] = self._is_not
4550
if expect_options.get("timeout") is None:
46-
expect_options["timeout"] = 5_000
51+
expect_options["timeout"] = self._timeout or 5_000
4752
if expect_options["isNot"]:
4853
message = message.replace("expected to", "expected not to")
4954
if "useInnerText" in expect_options and expect_options["useInnerText"] is None:
@@ -67,14 +72,20 @@ async def _expect_impl(
6772

6873
class PageAssertions(AssertionsBase):
6974
def __init__(
70-
self, page: Page, is_not: bool = False, message: Optional[str] = None
75+
self,
76+
page: Page,
77+
timeout: float = None,
78+
is_not: bool = False,
79+
message: Optional[str] = None,
7180
) -> None:
72-
super().__init__(page.locator(":root"), is_not, message)
81+
super().__init__(page.locator(":root"), timeout, is_not, message)
7382
self._actual_page = page
7483

7584
@property
7685
def _not(self) -> "PageAssertions":
77-
return PageAssertions(self._actual_page, not self._is_not, self._custom_message)
86+
return PageAssertions(
87+
self._actual_page, self._timeout, not self._is_not, self._custom_message
88+
)
7889

7990
async def to_have_title(
8091
self, title_or_reg_exp: Union[Pattern[str], str], timeout: float = None
@@ -120,15 +131,19 @@ async def not_to_have_url(
120131

121132
class LocatorAssertions(AssertionsBase):
122133
def __init__(
123-
self, locator: Locator, is_not: bool = False, message: Optional[str] = None
134+
self,
135+
locator: Locator,
136+
timeout: float = None,
137+
is_not: bool = False,
138+
message: Optional[str] = None,
124139
) -> None:
125-
super().__init__(locator, is_not, message)
140+
super().__init__(locator, timeout, is_not, message)
126141
self._actual_locator = locator
127142

128143
@property
129144
def _not(self) -> "LocatorAssertions":
130145
return LocatorAssertions(
131-
self._actual_locator, not self._is_not, self._custom_message
146+
self._actual_locator, self._timeout, not self._is_not, self._custom_message
132147
)
133148

134149
async def to_contain_text(
@@ -676,18 +691,23 @@ async def not_to_be_in_viewport(
676691

677692
class APIResponseAssertions:
678693
def __init__(
679-
self, response: APIResponse, is_not: bool = False, message: Optional[str] = None
694+
self,
695+
response: APIResponse,
696+
timeout: float = None,
697+
is_not: bool = False,
698+
message: Optional[str] = None,
680699
) -> None:
681700
self._loop = response._loop
682701
self._dispatcher_fiber = response._dispatcher_fiber
702+
self._timeout = timeout
683703
self._is_not = is_not
684704
self._actual = response
685705
self._custom_message = message
686706

687707
@property
688708
def _not(self) -> "APIResponseAssertions":
689709
return APIResponseAssertions(
690-
self._actual, not self._is_not, self._custom_message
710+
self._actual, self._timeout, not self._is_not, self._custom_message
691711
)
692712

693713
async def to_be_ok(

playwright/async_api/__init__.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -87,35 +87,50 @@ def async_playwright() -> PlaywrightContextManager:
8787
return PlaywrightContextManager()
8888

8989

90-
@overload
91-
def expect(actual: Page, message: Optional[str] = None) -> PageAssertions:
92-
...
93-
94-
95-
@overload
96-
def expect(actual: Locator, message: Optional[str] = None) -> LocatorAssertions:
97-
...
98-
99-
100-
@overload
101-
def expect(actual: APIResponse, message: Optional[str] = None) -> APIResponseAssertions:
102-
...
103-
104-
105-
def expect(
106-
actual: Union[Page, Locator, APIResponse], message: Optional[str] = None
107-
) -> Union[PageAssertions, LocatorAssertions, APIResponseAssertions]:
108-
if isinstance(actual, Page):
109-
return PageAssertions(PageAssertionsImpl(actual._impl_obj, message=message))
110-
elif isinstance(actual, Locator):
111-
return LocatorAssertions(
112-
LocatorAssertionsImpl(actual._impl_obj, message=message)
113-
)
114-
elif isinstance(actual, APIResponse):
115-
return APIResponseAssertions(
116-
APIResponseAssertionsImpl(actual._impl_obj, message=message)
117-
)
118-
raise ValueError(f"Unsupported type: {type(actual)}")
90+
class Expect:
91+
def __init__(self) -> None:
92+
self._timeout: Optional[float] = None
93+
94+
def set_timeout(self, timeout: float) -> None:
95+
self._timeout = timeout
96+
97+
@overload
98+
def __call__(self, actual: Page, message: Optional[str] = None) -> PageAssertions:
99+
...
100+
101+
@overload
102+
def __call__(
103+
self, actual: Locator, message: Optional[str] = None
104+
) -> LocatorAssertions:
105+
...
106+
107+
@overload
108+
def __call__(
109+
self, actual: APIResponse, message: Optional[str] = None
110+
) -> APIResponseAssertions:
111+
...
112+
113+
def __call__(
114+
self, actual: Union[Page, Locator, APIResponse], message: Optional[str] = None
115+
) -> Union[PageAssertions, LocatorAssertions, APIResponseAssertions]:
116+
if isinstance(actual, Page):
117+
return PageAssertions(
118+
PageAssertionsImpl(actual._impl_obj, self._timeout, message=message)
119+
)
120+
elif isinstance(actual, Locator):
121+
return LocatorAssertions(
122+
LocatorAssertionsImpl(actual._impl_obj, self._timeout, message=message)
123+
)
124+
elif isinstance(actual, APIResponse):
125+
return APIResponseAssertions(
126+
APIResponseAssertionsImpl(
127+
actual._impl_obj, self._timeout, message=message
128+
)
129+
)
130+
raise ValueError(f"Unsupported type: {type(actual)}")
131+
132+
133+
expect = Expect()
119134

120135

121136
__all__ = [

playwright/sync_api/__init__.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -87,35 +87,50 @@ def sync_playwright() -> PlaywrightContextManager:
8787
return PlaywrightContextManager()
8888

8989

90-
@overload
91-
def expect(actual: Page, message: Optional[str] = None) -> PageAssertions:
92-
...
93-
94-
95-
@overload
96-
def expect(actual: Locator, message: Optional[str] = None) -> LocatorAssertions:
97-
...
98-
99-
100-
@overload
101-
def expect(actual: APIResponse, message: Optional[str] = None) -> APIResponseAssertions:
102-
...
103-
104-
105-
def expect(
106-
actual: Union[Page, Locator, APIResponse], message: Optional[str] = None
107-
) -> Union[PageAssertions, LocatorAssertions, APIResponseAssertions]:
108-
if isinstance(actual, Page):
109-
return PageAssertions(PageAssertionsImpl(actual._impl_obj, message=message))
110-
elif isinstance(actual, Locator):
111-
return LocatorAssertions(
112-
LocatorAssertionsImpl(actual._impl_obj, message=message)
113-
)
114-
elif isinstance(actual, APIResponse):
115-
return APIResponseAssertions(
116-
APIResponseAssertionsImpl(actual._impl_obj, message=message)
117-
)
118-
raise ValueError(f"Unsupported type: {type(actual)}")
90+
class Expect:
91+
def __init__(self) -> None:
92+
self._timeout: Optional[float] = None
93+
94+
def set_timeout(self, timeout: float) -> None:
95+
self._timeout = timeout
96+
97+
@overload
98+
def __call__(self, actual: Page, message: Optional[str] = None) -> PageAssertions:
99+
...
100+
101+
@overload
102+
def __call__(
103+
self, actual: Locator, message: Optional[str] = None
104+
) -> LocatorAssertions:
105+
...
106+
107+
@overload
108+
def __call__(
109+
self, actual: APIResponse, message: Optional[str] = None
110+
) -> APIResponseAssertions:
111+
...
112+
113+
def __call__(
114+
self, actual: Union[Page, Locator, APIResponse], message: Optional[str] = None
115+
) -> Union[PageAssertions, LocatorAssertions, APIResponseAssertions]:
116+
if isinstance(actual, Page):
117+
return PageAssertions(
118+
PageAssertionsImpl(actual._impl_obj, self._timeout, message=message)
119+
)
120+
elif isinstance(actual, Locator):
121+
return LocatorAssertions(
122+
LocatorAssertionsImpl(actual._impl_obj, self._timeout, message=message)
123+
)
124+
elif isinstance(actual, APIResponse):
125+
return APIResponseAssertions(
126+
APIResponseAssertionsImpl(
127+
actual._impl_obj, self._timeout, message=message
128+
)
129+
)
130+
raise ValueError(f"Unsupported type: {type(actual)}")
131+
132+
133+
expect = Expect()
119134

120135

121136
__all__ = [

tests/async/test_assertions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,3 +790,21 @@ async def test_should_be_attached_over_navigation(page: Page, server: Server) ->
790790
await page.goto(server.PREFIX + "/input/checkbox.html")
791791
await task
792792
assert task.done()
793+
794+
795+
async def test_should_be_able_to_set_custom_timeout(page: Page) -> None:
796+
with pytest.raises(AssertionError) as exc_info:
797+
await expect(page.locator("#a1")).to_be_visible(timeout=111)
798+
assert "LocatorAssertions.to_be_visible with timeout 111ms" in str(exc_info.value)
799+
800+
801+
async def test_should_be_able_to_set_custom_global_timeout(page: Page) -> None:
802+
try:
803+
expect.set_timeout(111)
804+
with pytest.raises(AssertionError) as exc_info:
805+
await expect(page.locator("#a1")).to_be_visible()
806+
assert "LocatorAssertions.to_be_visible with timeout 111ms" in str(
807+
exc_info.value
808+
)
809+
finally:
810+
expect.set_timeout(None)

tests/sync/test_assertions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,3 +852,21 @@ def test_should_be_attached_with_impossible_timeout(page: Page) -> None:
852852
def test_should_be_attached_with_impossible_timeout_not(page: Page) -> None:
853853
page.set_content("<div id=node>Text content</div>")
854854
expect(page.locator("no-such-thing")).not_to_be_attached(timeout=1)
855+
856+
857+
def test_should_be_able_to_set_custom_timeout(page: Page) -> None:
858+
with pytest.raises(AssertionError) as exc_info:
859+
expect(page.locator("#a1")).to_be_visible(timeout=111)
860+
assert "LocatorAssertions.to_be_visible with timeout 111ms" in str(exc_info.value)
861+
862+
863+
def test_should_be_able_to_set_custom_global_timeout(page: Page) -> None:
864+
try:
865+
expect.set_timeout(111)
866+
with pytest.raises(AssertionError) as exc_info:
867+
expect(page.locator("#a1")).to_be_visible()
868+
assert "LocatorAssertions.to_be_visible with timeout 111ms" in str(
869+
exc_info.value
870+
)
871+
finally:
872+
expect.set_timeout(5_000)

0 commit comments

Comments
 (0)