Skip to content

Commit 72fb0e0

Browse files
authored
fix(remote): reject waiters before disconnecting (#3135)
1 parent 4ce52fc commit 72fb0e0

3 files changed

Lines changed: 83 additions & 31 deletions

File tree

playwright/_impl/_browser_type.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,15 +255,15 @@ def handle_transport_close(reason: Optional[str]) -> None:
255255
for page in context.pages:
256256
page._on_close()
257257
context._on_close()
258-
browser._on_close()
259258
connection.cleanup(reason)
260-
# TODO: Backport https://github.com/microsoft/playwright/commit/d8d5289e8692c9b1265d23ee66988d1ac5122f33
261259
# Give a chance to any API call promises to reject upon page/context closure.
262260
# This happens naturally when we receive page.onClose and browser.onClose from the server
263261
# in separate tasks. However, upon pipe closure we used to dispatch them all synchronously
264262
# here and promises did not have a chance to reject.
265263
# The order of rejects vs closure is a part of the API contract and our test runner
266264
# relies on it to attribute rejections to the right test.
265+
if browser:
266+
connection._loop.call_soon(browser._on_close)
267267

268268
transport.once("close", handle_transport_close)
269269

playwright/_impl/_waiter.py

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import math
1818
import uuid
1919
from asyncio.tasks import Task
20-
from typing import Any, Callable, List, Optional, Tuple, Union
20+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
2121

2222
from pyee import EventEmitter
2323

@@ -37,9 +37,7 @@ def __init__(self, channel_owner: ChannelOwner, event: str) -> None:
3737
self._wait_for_event_info_before(self._wait_id, event)
3838

3939
def _wait_for_event_info_before(self, wait_id: str, event: str) -> None:
40-
self._channel.send_no_reply(
41-
"__waitInfo__",
42-
None,
40+
self._send_wait_for_event_info(
4341
{
4442
"waitId": wait_id,
4543
"phase": "before",
@@ -49,18 +47,29 @@ def _wait_for_event_info_before(self, wait_id: str, event: str) -> None:
4947
)
5048

5149
def _wait_for_event_info_after(self, wait_id: str, error: Exception = None) -> None:
52-
self._channel._connection.wrap_api_call_sync(
53-
lambda: self._channel.send_no_reply(
50+
self._send_wait_for_event_info(
51+
{
52+
"waitId": wait_id,
53+
"phase": "after",
54+
**({"error": str(error)} if error else {}),
55+
},
56+
is_internal=True,
57+
)
58+
59+
def _send_wait_for_event_info(
60+
self, params: Dict[str, Any], is_internal: bool = False, title: str = None
61+
) -> None:
62+
# Wait info is fire-and-forget telemetry and must not affect the waiter.
63+
try:
64+
self._channel.send_no_reply(
5465
"__waitInfo__",
5566
None,
56-
{
57-
"waitId": wait_id,
58-
"phase": "after",
59-
**({"error": str(error)} if error else {}),
60-
},
61-
),
62-
True,
63-
)
67+
params,
68+
is_internal=is_internal,
69+
title=title,
70+
)
71+
except Error:
72+
pass
6473

6574
def reject_on_event(
6675
self,
@@ -159,21 +168,14 @@ def result(self) -> asyncio.Future:
159168

160169
def log(self, message: str) -> None:
161170
self._logs.append(message)
162-
try:
163-
self._channel._connection.wrap_api_call_sync(
164-
lambda: self._channel.send_no_reply(
165-
"__waitInfo__",
166-
None,
167-
{
168-
"waitId": self._wait_id,
169-
"phase": "log",
170-
"message": message,
171-
},
172-
),
173-
True,
174-
)
175-
except Exception:
176-
pass
171+
self._send_wait_for_event_info(
172+
{
173+
"waitId": self._wait_id,
174+
"phase": "log",
175+
"message": message,
176+
},
177+
is_internal=True,
178+
)
177179

178180

179181
def throw_on_timeout(timeout: float, exception: Exception) -> asyncio.Task:

tests/async/test_browsertype_connect.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,56 @@ async def test_browser_type_connect_should_reject_navigation_when_browser_closes
180180
assert "has been closed" in exc_info.value.message
181181

182182

183+
async def test_browser_type_connect_should_reject_wait_for_event_before_browser_close_finishes(
184+
browser_type: BrowserType, launch_server: Callable[[], RemoteServer]
185+
) -> None:
186+
remote_server = launch_server()
187+
browser = await browser_type.connect(remote_server.ws_endpoint)
188+
page = await browser.new_page()
189+
rejected = asyncio.Event()
190+
191+
async def wait_for_download() -> None:
192+
with pytest.raises(Error):
193+
await page.wait_for_event("download")
194+
rejected.set()
195+
196+
wait_task = asyncio.create_task(wait_for_download())
197+
await asyncio.sleep(0)
198+
await browser.close()
199+
200+
assert rejected.is_set()
201+
await wait_task
202+
203+
204+
async def test_browser_type_connect_should_reject_wait_for_event_before_disconnected(
205+
browser_type: BrowserType, launch_server: Callable[[], RemoteServer]
206+
) -> None:
207+
remote_server = launch_server()
208+
browser = await browser_type.connect(remote_server.ws_endpoint)
209+
page = await browser.new_page()
210+
log = []
211+
disconnected = asyncio.Event()
212+
213+
async def wait_for_download() -> None:
214+
with pytest.raises(Error):
215+
await page.wait_for_event("download")
216+
log.append("rejected")
217+
218+
def on_disconnected(_: object) -> None:
219+
log.append("disconnected")
220+
disconnected.set()
221+
222+
wait_task = asyncio.create_task(wait_for_download())
223+
await asyncio.sleep(0)
224+
browser.on("disconnected", on_disconnected)
225+
226+
remote_server.kill()
227+
await wait_task
228+
await asyncio.wait_for(disconnected.wait(), timeout=5)
229+
230+
assert log == ["rejected", "disconnected"]
231+
232+
183233
async def test_should_not_allow_getting_the_path(
184234
browser_type: BrowserType, launch_server: Callable[[], RemoteServer], server: Server
185235
) -> None:

0 commit comments

Comments
 (0)