diff --git a/src/widgetastic/browser.py b/src/widgetastic/browser.py index af90b25..55cd945 100644 --- a/src/widgetastic/browser.py +++ b/src/widgetastic/browser.py @@ -1619,6 +1619,46 @@ def expect_response(self, url_or_predicate, *, timeout: int = 10000): self.logger.debug("expect_response(timeout=%d)", timeout) return self.page.expect_response(url_or_predicate, timeout=timeout) + def expect_download(self, predicate=None, *, timeout: int = 30000): + """Context manager that waits for a download triggered by a page action. + + Performs an action inside the ``with`` block and waits for the browser + to start a file download. An optional *predicate* can narrow the match + to a specific download. + + Args: + predicate: Optional callable ``(Download) -> bool``. When given, + the ``Download`` object is passed to *predicate* and only a + truthy return is accepted as a match. + timeout: Maximum time to wait for the download to start in + milliseconds. Defaults to 30 000 ms (30 s). + + Returns: + A context manager whose ``.value`` attribute holds the matched + ``Download`` object after the block exits. + + Usage:: + + with browser.expect_download() as download_info: + view.download_button.click() + + download = download_info.value + assert ".csv" in download.suggested_filename + + # With a predicate + with browser.expect_download( + lambda d: d.suggested_filename.endswith(".pdf"), + ) as download_info: + view.export_pdf_button.click() + + download_info.value.save_as("/tmp/report.pdf") + """ + self.logger.debug("expect_download(timeout=%d)", timeout) + kwargs = {"timeout": timeout} + if predicate is not None: + kwargs["predicate"] = predicate + return self.page.expect_download(**kwargs) + # ====================== ALERT HANDLING (TODO/FUTURE) ====================== # TODO: Implement alert handling # def get_alert(self) -> Alert: diff --git a/testing/test_browser.py b/testing/test_browser.py index cf498be..325e710 100644 --- a/testing/test_browser.py +++ b/testing/test_browser.py @@ -1310,6 +1310,41 @@ def test_expect_response(browser): browser.page.unroute(mock_url) +def test_expect_download(browser): + """Test expect_download captures a file download triggered by JS.""" + with browser.expect_download() as download_info: + browser.execute_script( + """ + const a = document.createElement('a'); + a.href = URL.createObjectURL(new Blob(['test content'], {type: 'text/plain'})); + a.download = 'test_file.txt'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + """ + ) + download = download_info.value + assert download.suggested_filename == "test_file.txt" + + +def test_expect_download_with_predicate(browser): + """Test expect_download with a predicate to match specific downloads.""" + with browser.expect_download( + predicate=lambda d: d.suggested_filename.endswith(".csv"), + ) as download_info: + browser.execute_script( + """ + const a = document.createElement('a'); + a.href = URL.createObjectURL(new Blob(['a,b,c'], {type: 'text/csv'})); + a.download = 'report.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + """ + ) + assert download_info.value.suggested_filename == "report.csv" + + # =================== OVERALL FUNCTIONALITY & BrowserParentWrapper TESTS ===================