Repository: microsoft/playwright-python

Issue:
Future exception was never retrieved when closing page waiting for download event
I'm using playwright to download PDFs of URLs from RSS feeds. Some of the URLs are actually links to PDFs (mixed with links to "normal" webpages), and I'd like to handle that by downloading the PDFs. I have an implementation which works, I've included a minimal(ish) version below which accepts a URL and a filename to write it to. You can try for example (where `script.py` contains the code below):

Convert a web page to PDF: `python3 script.py https://arxiv.org/abs/1912.11035 a.pdf`
Download a PDF: `python3 script.py https://arxiv.org/pdf/1912.11035 b.pdf`

The first example (converting a webpage to a PDF) outputs this:
```
goto success: https://arxiv.org/abs/1912.11035
download exception: https://arxiv.org/abs/1912.11035: Page closed
Future exception was never retrieved
future: <Future finished exception=Error('Target page, context or browser has been closed')>
playwright._impl._api_types.Error: Target page, context or browser has been closed
```
I can't figure out how to stop the "Future exception was never retrieved" warning being printed. As you can see the "Page closed" exception has been caught in the exception handler for `await download_task`.

Am I doing something wrong? Or is this an issue in the playwright code?

```
import sys
import asyncio
from playwright.async_api import async_playwright

async def download(url, filename):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context(accept_downloads = True, java_script_enabled = False)
        page = await context.new_page()

        download_task = asyncio.create_task(page.wait_for_event('download'))
        goto_task = asyncio.create_task(page.goto(url, wait_until='networkidle'))
        try:
            await goto_task
            await page.pdf(path=filename)
            print('goto success: ' + url)
            await page.close()
            success = True
        except Exception as e:
            print('goto exception: {}: {}'.format(url, e))

        try:
            download = await download_task
            await download.save_as(filename)
            print('download success: ' + url)
            await page.close()
            success = True
        except Exception as e:
            print('download exception: {}: {}'.format(url, e))

        if not success:
            await page.close()

if __name__ == '__main__':
    asyncio.run(download(sys.argv[1], sys.argv[2]))

```


Discussion:
This looks like unexpected behaviour, but since this is about the logic which gets rewritten in #820 I will check it after #820 got fixed (soon). Thanks for your great reproducible!


Resolving patch:
diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py
--- a/playwright/_impl/_connection.py
+++ b/playwright/_impl/_connection.py
@@ -99,31 +99,6 @@ def __init__(
         if self._parent:
             self._parent._objects[guid] = self
 
-    def _wait_for_event_info_before(self, wait_id: str, api_name: str) -> None:
-        self._connection._send_message_to_server(
-            self._guid,
-            "waitForEventInfo",
-            {
-                "info": {
-                    "apiName": api_name,
-                    "waitId": wait_id,
-                    "phase": "before",
-                }
-            },
-        )
-
-    def _wait_for_event_info_after(
-        self, wait_id: str, exception: Exception = None
-    ) -> None:
-        info = {"waitId": wait_id, "phase": "after"}
-        if exception:
-            info["error"] = str(exception)
-        self._connection._send_message_to_server(
-            self._guid,
-            "waitForEventInfo",
-            {"info": info},
-        )
-
     def _dispose(self) -> None:
         # Clean up from parent and connection.
         if self._parent:
diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py
--- a/playwright/_impl/_frame.py
+++ b/playwright/_impl/_frame.py
@@ -122,7 +122,7 @@ async def goto(
     def _setup_navigation_wait_helper(
         self, wait_name: str, timeout: float = None
     ) -> WaitHelper:
-        wait_helper = WaitHelper(self, f"frame.{wait_name}")
+        wait_helper = WaitHelper(self._page, f"frame.{wait_name}")
         wait_helper.reject_on_event(
             self._page, "close", Error("Navigation failed because page was closed!")
         )
diff --git a/playwright/_impl/_wait_helper.py b/playwright/_impl/_wait_helper.py
--- a/playwright/_impl/_wait_helper.py
+++ b/playwright/_impl/_wait_helper.py
@@ -24,14 +24,41 @@
 
 
 class WaitHelper:
-    def __init__(self, channel_owner: ChannelOwner, api_name: str) -> None:
+    def __init__(self, channel_owner: ChannelOwner, event: str) -> None:
         self._result: asyncio.Future = asyncio.Future()
         self._wait_id = uuid.uuid4().hex
         self._loop = channel_owner._loop
         self._pending_tasks: List[Task] = []
-        self._channel_owner = channel_owner
+        self._channel = channel_owner._channel
         self._registered_listeners: List[Tuple[EventEmitter, str, Callable]] = []
-        channel_owner._wait_for_event_info_before(self._wait_id, api_name)
+        self._wait_for_event_info_before(self._wait_id, event)
+
+    def _wait_for_event_info_before(self, wait_id: str, event: str) -> None:
+        self._channel.send_no_reply(
+            "waitForEventInfo",
+            {
+                "info": {
+                    "waitId": wait_id,
+                    "phase": "before",
+                    "event": event,
+                }
+            },
+        )
+
+    def _wait_for_event_info_after(self, wait_id: str, error: Exception = None) -> None:
+        try:
+            self._channel.send_no_reply(
+                "waitForEventInfo",
+                {
+                    "info": {
+                        "waitId": wait_id,
+                        "phase": "after",
+                        "error": str(error) if error else None,
+                    }
+                },
+            )
+        except Exception:
+            pass
 
     def reject_on_event(
         self,
@@ -68,13 +95,13 @@ def _fulfill(self, result: Any) -> None:
         self._cleanup()
         if not self._result.done():
             self._result.set_result(result)
-        self._channel_owner._wait_for_event_info_after(self._wait_id)
+        self._wait_for_event_info_after(self._wait_id)
 
     def _reject(self, exception: Exception) -> None:
         self._cleanup()
         if not self._result.done():
             self._result.set_exception(exception)
-        self._channel_owner._wait_for_event_info_after(self._wait_id, exception)
+        self._wait_for_event_info_after(self._wait_id, exception)
 
     def wait_for_event(
         self,


Test patch:
diff --git a/tests/assets/networkidle.html b/tests/assets/networkidle.html
new file mode 100644
--- /dev/null
+++ b/tests/assets/networkidle.html
@@ -0,0 +1 @@
+<script src='networkidle.js'></script>
diff --git a/tests/assets/networkidle.js b/tests/assets/networkidle.js
new file mode 100644
--- /dev/null
+++ b/tests/assets/networkidle.js
@@ -0,0 +1,12 @@
+async function main() {
+  window.ws = new WebSocket('ws://localhost:' + window.location.port + '/ws');
+  window.ws.addEventListener('message', message => {});
+
+  fetch('fetch-request-a.js');
+  window.top.fetchSecond = () => {
+    // Do not return the promise here.
+    fetch('fetch-request-b.js');
+  };
+}
+
+main();
diff --git a/tests/async/test_accessibility.py b/tests/async/test_accessibility.py
--- a/tests/async/test_accessibility.py
+++ b/tests/async/test_accessibility.py
@@ -221,29 +221,17 @@ async def test_accessibility_filtering_children_of_leaf_nodes_rich_text_editable
             "children": [{"role": "text", "name": "my fake image"}],
         }
     else:
-        if browser_channel:
-            golden = {
-                "role": "textbox",
-                "name": "",
-                "value": "Edit this image: ",
-                "children": [
-                    {"role": "text", "name": "Edit this image:"},
-                    {"role": "img", "name": "my fake image"},
-                ],
-                "value": "Edit this image: ",
-            }
-        else:
-            golden = {
-                "role": "textbox",
-                "name": "",
-                "multiline": True,
-                "value": "Edit this image: ",
-                "children": [
-                    {"role": "text", "name": "Edit this image:"},
-                    {"role": "img", "name": "my fake image"},
-                ],
-                "value": "Edit this image: ",
-            }
+        golden = {
+            "role": "textbox",
+            "name": "",
+            "multiline": True,
+            "value": "Edit this image: ",
+            "children": [
+                {"role": "text", "name": "Edit this image:"},
+                {"role": "img", "name": "my fake image"},
+            ],
+            "value": "Edit this image: ",
+        }
     snapshot = await page.accessibility.snapshot()
     assert snapshot["children"][0] == golden
 
@@ -259,19 +247,12 @@ async def test_accessibility_plain_text_field_with_role_should_not_have_children
     <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>"""
     )
     snapshot = await page.accessibility.snapshot()
-    if browser_channel:
-        assert snapshot["children"][0] == {
-            "name": "",
-            "role": "textbox",
-            "value": "Edit this image:",
-        }
-    else:
-        assert snapshot["children"][0] == {
-            "multiline": True,
-            "name": "",
-            "role": "textbox",
-            "value": "Edit this image:",
-        }
+    assert snapshot["children"][0] == {
+        "multiline": True,
+        "name": "",
+        "role": "textbox",
+        "value": "Edit this image:",
+    }
 
 
 @pytest.mark.only_browser("chromium")
@@ -283,14 +264,11 @@ async def test_accessibility_plain_text_field_without_role_should_not_have_conte
     <div contenteditable="plaintext-only">Edit this image:<img src="fakeimage.png" alt="my fake image"></div>"""
     )
     snapshot = await page.accessibility.snapshot()
-    if browser_channel:
-        assert snapshot["children"][0] == {"name": "", "role": "generic"}
-    else:
-        assert snapshot["children"][0] == {
-            "name": "",
-            "role": "generic",
-            "value": "Edit this image:",
-        }
+    assert snapshot["children"][0] == {
+        "name": "",
+        "role": "generic",
+        "value": "Edit this image:",
+    }
 
 
 @pytest.mark.only_browser("chromium")
@@ -302,14 +280,11 @@ async def test_accessibility_plain_text_field_with_tabindex_and_without_role_sho
     <div contenteditable="plaintext-only" tabIndex=0>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>"""
     )
     snapshot = await page.accessibility.snapshot()
-    if browser_channel:
-        assert snapshot["children"][0] == {"name": "", "role": "generic"}
-    else:
-        assert snapshot["children"][0] == {
-            "name": "",
-            "role": "generic",
-            "value": "Edit this image:",
-        }
+    assert snapshot["children"][0] == {
+        "name": "",
+        "role": "generic",
+        "value": "Edit this image:",
+    }
 
 
 async def test_accessibility_non_editable_textbox_with_role_and_tabIndex_and_label_should_not_have_children(
diff --git a/tests/async/test_navigation.py b/tests/async/test_navigation.py
--- a/tests/async/test_navigation.py
+++ b/tests/async/test_navigation.py
@@ -19,7 +19,8 @@
 
 import pytest
 
-from playwright.async_api import Error, Request, TimeoutError
+from playwright.async_api import Error, Page, Request, TimeoutError
+from tests.server import Server
 
 
 async def test_goto_should_work(page, server):
@@ -676,6 +677,14 @@ def handler(request: Any):
     await page.wait_for_load_state("domcontentloaded")
 
 
+async def test_wait_for_load_state_networkidle(page: Page, server: Server):
+    wait_for_network_idle_future = asyncio.create_task(
+        page.wait_for_load_state("networkidle")
+    )
+    await page.goto(server.PREFIX + "/networkidle.html")
+    await wait_for_network_idle_future
+
+
 async def test_wait_for_load_state_should_work_with_pages_that_have_loaded_before_being_connected_to(
     page, context, server
 ):
diff --git a/tests/sync/test_accessibility.py b/tests/sync/test_accessibility.py
--- a/tests/sync/test_accessibility.py
+++ b/tests/sync/test_accessibility.py
@@ -215,29 +215,17 @@ def test_accessibility_filtering_children_of_leaf_nodes_rich_text_editable_field
             "children": [{"role": "text", "name": "my fake image"}],
         }
     else:
-        if browser_channel:
-            golden = {
-                "role": "textbox",
-                "name": "",
-                "value": "Edit this image: ",
-                "children": [
-                    {"role": "text", "name": "Edit this image:"},
-                    {"role": "img", "name": "my fake image"},
-                ],
-                "value": "Edit this image: ",
-            }
-        else:
-            golden = {
-                "role": "textbox",
-                "name": "",
-                "multiline": True,
-                "value": "Edit this image: ",
-                "children": [
-                    {"role": "text", "name": "Edit this image:"},
-                    {"role": "img", "name": "my fake image"},
-                ],
-                "value": "Edit this image: ",
-            }
+        golden = {
+            "role": "textbox",
+            "name": "",
+            "multiline": True,
+            "value": "Edit this image: ",
+            "children": [
+                {"role": "text", "name": "Edit this image:"},
+                {"role": "img", "name": "my fake image"},
+            ],
+            "value": "Edit this image: ",
+        }
     snapshot = page.accessibility.snapshot()
     assert snapshot["children"][0] == golden
 
@@ -253,19 +241,12 @@ def test_accessibility_plain_text_field_with_role_should_not_have_children(
     <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>"""
     )
     snapshot = page.accessibility.snapshot()
-    if browser_channel:
-        assert snapshot["children"][0] == {
-            "name": "",
-            "role": "textbox",
-            "value": "Edit this image:",
-        }
-    else:
-        assert snapshot["children"][0] == {
-            "multiline": True,
-            "name": "",
-            "role": "textbox",
-            "value": "Edit this image:",
-        }
+    assert snapshot["children"][0] == {
+        "multiline": True,
+        "name": "",
+        "role": "textbox",
+        "value": "Edit this image:",
+    }
 
 
 @pytest.mark.only_browser("chromium")
@@ -277,14 +258,11 @@ def test_accessibility_plain_text_field_without_role_should_not_have_content(
     <div contenteditable="plaintext-only">Edit this image:<img src="fakeimage.png" alt="my fake image"></div>"""
     )
     snapshot = page.accessibility.snapshot()
-    if browser_channel:
-        assert snapshot["children"][0] == {"name": "", "role": "generic"}
-    else:
-        assert snapshot["children"][0] == {
-            "name": "",
-            "role": "generic",
-            "value": "Edit this image:",
-        }
+    assert snapshot["children"][0] == {
+        "name": "",
+        "role": "generic",
+        "value": "Edit this image:",
+    }
 
 
 @pytest.mark.only_browser("chromium")
@@ -296,14 +274,11 @@ def test_accessibility_plain_text_field_with_tabindex_and_without_role_should_no
     <div contenteditable="plaintext-only" tabIndex=0>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>"""
     )
     snapshot = page.accessibility.snapshot()
-    if browser_channel:
-        assert snapshot["children"][0] == {"name": "", "role": "generic"}
-    else:
-        assert snapshot["children"][0] == {
-            "name": "",
-            "role": "generic",
-            "value": "Edit this image:",
-        }
+    assert snapshot["children"][0] == {
+        "name": "",
+        "role": "generic",
+        "value": "Edit this image:",
+    }
 
 
 def test_accessibility_non_editable_textbox_with_role_and_tabIndex_and_label_should_not_have_children(