diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 40ac78449..a1adf62d9 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -142,13 +142,21 @@ async def _inner_send( callback = self._connection._send_message_to_server( self._object, method, augmented_params, timeout ) - done, _ = await asyncio.wait( - { - self._connection._transport.on_error_future, - callback.future, - }, - return_when=asyncio.FIRST_COMPLETED, - ) + try: + done, _ = await asyncio.wait( + { + self._connection._transport.on_error_future, + callback.future, + }, + return_when=asyncio.FIRST_COMPLETED, + ) + except asyncio.CancelledError as exc: + await self._connection._abort( + self._object, + callback, + str(exc) or "Task was cancelled", + ) + raise if not callback.future.done(): callback.future.cancel() result = next(iter(done)).result() @@ -240,7 +248,10 @@ def remove_listener(self, event: str, f: Any) -> None: class ProtocolCallback: - def __init__(self, loop: asyncio.AbstractEventLoop, no_reply: bool = False) -> None: + def __init__( + self, loop: asyncio.AbstractEventLoop, id: int, no_reply: bool = False + ) -> None: + self.id = id self.stack_trace: traceback.StackSummary self.no_reply = no_reply self.future = loop.create_future() @@ -389,7 +400,7 @@ def _send_message_to_server( ) self._last_id += 1 id = self._last_id - callback = ProtocolCallback(self._loop, no_reply=no_reply) + callback = ProtocolCallback(self._loop, id, no_reply=no_reply) task = asyncio.current_task(self._loop) callback.stack_trace = cast( traceback.StackSummary, @@ -433,6 +444,34 @@ def _send_message_to_server( return callback + async def _abort( + self, object: ChannelOwner, callback: ProtocolCallback, reason: str + ) -> None: + try: + self._transport.send( + { + "guid": object._guid, + "method": "__abort__", + "params": {"id": callback.id, "reason": reason}, + } + ) + except (Error, OSError): + pass + try: + done, _ = await asyncio.wait( + { + self._transport.on_error_future, + callback.future, + }, + return_when=asyncio.FIRST_COMPLETED, + ) + finally: + if not callback.future.done(): + callback.future.cancel() + for future in done: + if not future.cancelled(): + future.exception() + def dispatch(self, msg: ParsedMessagePayload) -> None: if self._closed_error: return diff --git a/tests/async/test_asyncio.py b/tests/async/test_asyncio.py index c78648430..243a0eed8 100644 --- a/tests/async/test_asyncio.py +++ b/tests/async/test_asyncio.py @@ -27,7 +27,8 @@ async def test_should_cancel_underlying_protocol_calls( - browser_name: str, launch_arguments: Dict + browser_name: str, + launch_arguments: Dict, ) -> None: handler_exception = None @@ -40,12 +41,23 @@ def exception_handlerdler(loop: asyncio.AbstractEventLoop, context: Dict) -> Non async with async_playwright() as p: browser = await p[browser_name].launch(**launch_arguments) page = await browser.new_page() - task = asyncio.create_task(page.wait_for_selector("will-never-find")) - # make sure that the wait_for_selector message was sent to the server (driver) - await asyncio.sleep(0.1) + await page.set_content( + """ + + + """ + ) + task = asyncio.create_task(page.locator("button").click(timeout=0)) + await page.wait_for_timeout(100) + assert not task.done() + task.cancel() with pytest.raises(asyncio.CancelledError): await task + + await page.locator("button").evaluate("button => button.disabled = false") + await page.wait_for_timeout(700) + assert not await page.evaluate("window.clicked") await browser.close() # The actual 'Future exception was never retrieved' is logged inside the Future destructor (__del__).