Skip to content

Commit f29091a

Browse files
committed
test: add sync test variants and make DisposableStub a context manager
1 parent e763f89 commit f29091a

9 files changed

Lines changed: 288 additions & 8 deletions

File tree

playwright/_impl/_browser_context.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,9 @@ async def route(
438438
),
439439
)
440440
await self._update_interception_patterns()
441-
return DisposableStub(lambda: self.unroute(url, handler))
441+
return DisposableStub(
442+
lambda: self.unroute(url, handler), self._loop, self._dispatcher_fiber
443+
)
442444

443445
async def unroute(
444446
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None

playwright/_impl/_disposable.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import asyncio
16+
import inspect
17+
import traceback
18+
from asyncio import AbstractEventLoop
1519
from typing import Awaitable, Callable, Dict
1620

21+
import greenlet
22+
1723
from playwright._impl._connection import ChannelOwner
18-
from playwright._impl._errors import is_target_closed_error
24+
from playwright._impl._errors import Error, is_target_closed_error
1925

2026

2127
class Disposable(ChannelOwner):
@@ -42,8 +48,15 @@ def __repr__(self) -> str:
4248

4349

4450
class DisposableStub:
45-
def __init__(self, dispose_fn: Callable[[], Awaitable[None]]) -> None:
51+
def __init__(
52+
self,
53+
dispose_fn: Callable[[], Awaitable[None]],
54+
loop: AbstractEventLoop,
55+
dispatcher_fiber: object,
56+
) -> None:
4657
self._dispose_fn = dispose_fn
58+
self._loop = loop
59+
self._dispatcher_fiber = dispatcher_fiber
4760

4861
async def dispose(self) -> None:
4962
await self._dispose_fn()
@@ -54,6 +67,25 @@ async def __aenter__(self) -> "DisposableStub":
5467
async def __aexit__(self, *args: object) -> None:
5568
await self.dispose()
5669

70+
def __enter__(self) -> "DisposableStub":
71+
return self
72+
73+
def __exit__(self, *args: object) -> None:
74+
self._sync(self.dispose())
75+
76+
def _sync(self, coro: object) -> object:
77+
if self._loop.is_closed():
78+
raise Error("Event loop is closed! Is Playwright already stopped?")
79+
g_self = greenlet.getcurrent()
80+
task = self._loop.create_task(coro) # type: ignore
81+
setattr(task, "__pw_stack__", inspect.stack(0))
82+
setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10))
83+
task.add_done_callback(lambda _: g_self.switch())
84+
while not task.done():
85+
self._dispatcher_fiber.switch() # type: ignore
86+
asyncio._set_running_loop(self._loop)
87+
return task.result()
88+
5789
async def close(self) -> None:
5890
await self.dispose()
5991

playwright/_impl/_page.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,9 @@ async def route(
690690
),
691691
)
692692
await self._update_interception_patterns()
693-
return DisposableStub(lambda: self.unroute(url, handler))
693+
return DisposableStub(
694+
lambda: self.unroute(url, handler), self._loop, self._dispatcher_fiber
695+
)
694696

695697
async def unroute(
696698
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None

playwright/_impl/_screencast.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Screencast:
3636
def __init__(self, page: "Page") -> None:
3737
self._page = page
3838
self._loop = page._loop
39+
self._dispatcher_fiber = page._dispatcher_fiber
3940
self._started = False
4041
self._save_path: Optional[Union[str, Path]] = None
4142
self._on_frame: Optional[Callable[[ScreencastFrame], None]] = None
@@ -70,7 +71,7 @@ async def start(
7071
self._artifact = from_channel(result)
7172
self._save_path = path
7273

73-
return DisposableStub(lambda: self.stop())
74+
return DisposableStub(lambda: self.stop(), self._loop, self._dispatcher_fiber)
7475

7576
async def stop(self) -> None:
7677
self._started = False
@@ -94,7 +95,9 @@ async def show_actions(
9495
None,
9596
{"duration": duration, "position": position, "fontSize": fontSize},
9697
)
97-
return DisposableStub(lambda: self.hide_actions())
98+
return DisposableStub(
99+
lambda: self.hide_actions(), self._loop, self._dispatcher_fiber
100+
)
98101

99102
async def hide_actions(self) -> None:
100103
await self._page._channel.send("screencastHideActions", None)
@@ -111,7 +114,9 @@ async def show_overlay(self, html: str, duration: float = None) -> DisposableStu
111114
"screencastRemoveOverlay",
112115
None,
113116
{"id": overlay_id},
114-
)
117+
),
118+
self._loop,
119+
self._dispatcher_fiber,
115120
)
116121

117122
async def show_chapter(

playwright/_impl/_tracing.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ async def group(
144144
self, name: str, location: TracingGroupLocation = None
145145
) -> DisposableStub:
146146
await self._channel.send("tracingGroup", None, locals_to_params(locals()))
147-
return DisposableStub(lambda: self.group_end())
147+
return DisposableStub(
148+
lambda: self.group_end(), self._loop, self._dispatcher_fiber
149+
)
148150

149151
async def group_end(self) -> None:
150152
await self._channel.send(

tests/async/test_debugger.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ async def test_should_pause_at_next_and_resume(
3434
await dbg.request_pause()
3535

3636
paused_event = asyncio.Event()
37+
handler_called = [0]
3738

3839
def on_paused_state_changed() -> None:
40+
handler_called[0] += 1
3941
if dbg.paused_details is not None:
4042
assert "Click" in dbg.paused_details["title"]
4143
paused_event.set()
@@ -47,6 +49,7 @@ def on_paused_state_changed() -> None:
4749
await asyncio.wait_for(paused_event.wait(), timeout=10)
4850
await asyncio.wait_for(click_task, timeout=10)
4951
assert dbg.paused_details is None
52+
assert handler_called[0] > 0
5053

5154

5255
async def test_should_step_with_next(page: Page, context: BrowserContext) -> None:
@@ -57,8 +60,10 @@ async def test_should_step_with_next(page: Page, context: BrowserContext) -> Non
5760
await dbg.request_pause()
5861

5962
first_pause_seen = [False]
63+
handler_called = [0]
6064

6165
def on_paused_state_changed() -> None:
66+
handler_called[0] += 1
6267
if dbg.paused_details is not None and not first_pause_seen[0]:
6368
assert "Click" in dbg.paused_details["title"]
6469
first_pause_seen[0] = True
@@ -71,6 +76,7 @@ def on_paused_state_changed() -> None:
7176
click_task = asyncio.ensure_future(page.click("div"))
7277
await asyncio.wait_for(click_task, timeout=10)
7378
assert dbg.paused_details is None
79+
assert handler_called[0] > 0
7480

7581

7682
async def test_should_pause_at_pause_call(page: Page, context: BrowserContext) -> None:
@@ -81,8 +87,10 @@ async def test_should_pause_at_pause_call(page: Page, context: BrowserContext) -
8187
await dbg.request_pause()
8288

8389
paused_event = asyncio.Event()
90+
handler_called = [0]
8491

8592
def on_paused_state_changed() -> None:
93+
handler_called[0] += 1
8694
if dbg.paused_details is not None:
8795
assert "Pause" in dbg.paused_details["title"]
8896
paused_event.set()
@@ -94,3 +102,4 @@ def on_paused_state_changed() -> None:
94102
await asyncio.wait_for(paused_event.wait(), timeout=10)
95103
await asyncio.wait_for(pause_task, timeout=10)
96104
assert dbg.paused_details is None
105+
assert handler_called[0] > 0

tests/sync/test_browser_bind.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from playwright.sync_api import Browser
16+
17+
18+
def test_should_bind_and_unbind_browser(browser: Browser) -> None:
19+
server_info = browser.bind("default")
20+
try:
21+
assert isinstance(server_info["endpoint"], str)
22+
finally:
23+
browser.unbind()
24+
25+
26+
def test_should_bind_with_custom_host_and_port(browser: Browser) -> None:
27+
server_info = browser.bind("my-title", host="127.0.0.1", port=0)
28+
try:
29+
assert isinstance(server_info["endpoint"], str)
30+
finally:
31+
browser.unbind()

tests/sync/test_debugger.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from playwright.sync_api import BrowserContext, Page
16+
17+
18+
def test_should_return_none_paused_details_initially(
19+
context: BrowserContext,
20+
) -> None:
21+
dbg = context.debugger
22+
assert dbg.paused_details is None
23+
24+
25+
def test_should_pause_at_next_and_resume(page: Page, context: BrowserContext) -> None:
26+
page.set_content("<div>click me</div>")
27+
dbg = context.debugger
28+
assert dbg.paused_details is None
29+
30+
dbg.request_pause()
31+
32+
handler_called = [0]
33+
34+
def on_paused_state_changed() -> None:
35+
handler_called[0] += 1
36+
if dbg.paused_details is not None:
37+
assert "Click" in dbg.paused_details["title"]
38+
dbg.resume()
39+
40+
dbg.on("pausedstatechanged", on_paused_state_changed)
41+
42+
page.click("div")
43+
assert dbg.paused_details is None
44+
assert handler_called[0] > 0
45+
46+
47+
def test_should_step_with_next(page: Page, context: BrowserContext) -> None:
48+
page.set_content("<div>click me</div>")
49+
dbg = context.debugger
50+
assert dbg.paused_details is None
51+
52+
dbg.request_pause()
53+
54+
first_pause_seen = [False]
55+
handler_called = [0]
56+
57+
def on_paused_state_changed() -> None:
58+
handler_called[0] += 1
59+
if dbg.paused_details is not None and not first_pause_seen[0]:
60+
assert "Click" in dbg.paused_details["title"]
61+
first_pause_seen[0] = True
62+
dbg.next()
63+
elif dbg.paused_details is not None and first_pause_seen[0]:
64+
dbg.resume()
65+
66+
dbg.on("pausedstatechanged", on_paused_state_changed)
67+
68+
page.click("div")
69+
assert dbg.paused_details is None
70+
assert handler_called[0] > 0
71+
72+
73+
def test_should_pause_at_pause_call(page: Page, context: BrowserContext) -> None:
74+
page.set_content("<div>click me</div>")
75+
dbg = context.debugger
76+
assert dbg.paused_details is None
77+
78+
dbg.request_pause()
79+
80+
handler_called = [0]
81+
82+
def on_paused_state_changed() -> None:
83+
handler_called[0] += 1
84+
if dbg.paused_details is not None:
85+
assert "Pause" in dbg.paused_details["title"]
86+
dbg.resume()
87+
88+
dbg.on("pausedstatechanged", on_paused_state_changed)
89+
90+
page.pause()
91+
assert dbg.paused_details is None
92+
assert handler_called[0] > 0

0 commit comments

Comments
 (0)