Repository: microsoft/playwright-python

Issue:
fix: handle cancelled tasks
prevents `InvalidStateError` when the task has been cancelled, here:

```python
callback.future.set_exception(parsed_error)
```


Discussion:
Do you have a reproduction for this bug? `callback.future.set_exception` and `callback.future.set_result` are in different branches of if, they can't both happen. 
```python
from asyncio import FIRST_COMPLETED, CancelledError, create_task, run, wait
from contextlib import suppress
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("http://playwright.dev")
        done, pending = await wait({
            create_task(page.wait_for_selector("input[placeholder='search' i]")),
            create_task(page.wait_for_selector("input[placeholder='will-never-find' i]")),
        }, return_when=FIRST_COMPLETED)
        for task in pending:
            task.cancel()
            with suppress(CancelledError):
                await task

        await browser.close()

run(main())
```

```log
Task exception was never retrieved
future: <Task finished name='Task-2' coro=<Connection.run() done, defined at lib/python3.9/site-packages/playwright/_impl/_connection.py:163> exception=InvalidStateError('invalid state')>
Traceback (most recent call last):
  File "lib/python3.9/site-packages/playwright/_impl/_connection.py", line 166, in run
    await self._transport.run()
  File "lib/python3.9/site-packages/playwright/_impl/_transport.py", line 90, in run
    self.on_message(obj)
  File "lib/python3.9/site-packages/playwright/_impl/_connection.py", line 150, in <lambda>
    self._transport.on_message = lambda msg: self._dispatch(msg)
  File "lib/python3.9/site-packages/playwright/_impl/_connection.py", line 229, in _dispatch
    callback.future.set_exception(parsed_error)
asyncio.exceptions.InvalidStateError: invalid state
```
Got it, I did not realize the task was canceled externally. Would you mind adding this as a test for it?
I picked it up in #624 so it lands earlier. Thanks for the repo and explanation!

Resolving patch:
diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py
--- a/playwright/_impl/_connection.py
+++ b/playwright/_impl/_connection.py
@@ -220,6 +220,8 @@ def _dispatch(self, msg: ParsedMessagePayload) -> None:
         id = msg.get("id")
         if id:
             callback = self._callbacks.pop(id)
+            if callback.future.cancelled():
+                return
             error = msg.get("error")
             if error:
                 parsed_error = parse_error(error["error"])  # type: ignore


Test patch:
diff --git a/tests/async/test_issues.py b/tests/async/test_issues.py
--- a/tests/async/test_issues.py
+++ b/tests/async/test_issues.py
@@ -12,8 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from asyncio import FIRST_COMPLETED, CancelledError, create_task, wait
+
 import pytest
 
+from playwright.async_api import Page
+
 
 @pytest.mark.only_browser("chromium")
 async def test_issue_189(browser_type, launch_arguments):
@@ -30,3 +34,21 @@ async def test_issue_195(playwright, browser):
     iphone_11 = playwright.devices["iPhone 11"]
     context = await browser.new_context(**iphone_11)
     await context.close()
+
+
+async def test_connection_task_cancel(page: Page):
+    await page.set_content("<input />")
+    done, pending = await wait(
+        {
+            create_task(page.wait_for_selector("input")),
+            create_task(page.wait_for_selector("#will-never-resolve")),
+        },
+        return_when=FIRST_COMPLETED,
+    )
+    assert len(done) == 1
+    assert len(pending) == 1
+    for task in pending:
+        task.cancel()
+        with pytest.raises(CancelledError):
+            await task
+    assert list(pending)[0].cancelled()