Summary
A download triggered by window.open() never reaches page.expect_download(). Playwright attributes such a download to the opener page and resolves normally; rustwright waits out the timeout.
Downloads triggered by a same-tab navigation (location.href = url), by an <a download> element, or by a blob URL all work correctly — only the window.open() path is affected.
Repro
Deterministic, local, no network. The server responds to /file.txt with Content-Disposition: attachment, so the popup never becomes a page — it becomes a download.
import asyncio, http.server, socket, sys, threading
PAGE = b"""<!doctype html><html><body>opener</body></html>"""
class H(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if self.path.startswith("/file.txt"):
body = b"file contents"
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Disposition", 'attachment; filename="hello.txt"')
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(PAGE)))
self.end_headers()
self.wfile.write(PAGE)
def log_message(self, *a): pass
def serve():
s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close()
srv = http.server.ThreadingHTTPServer(("127.0.0.1", p), H); srv.daemon_threads = True
threading.Thread(target=srv.serve_forever, daemon=True).start()
return p
async def run(module, port):
api = __import__(f"{module}.async_api", fromlist=["async_playwright"])
async with api.async_playwright() as pw:
b = await pw.chromium.launch(headless=True)
for label, js in (
("window.open(url, '_blank')", "() => window.open('/file.txt', '_blank')"),
("window.open(url)", "() => window.open('/file.txt')"),
("location.href", "() => { window.location.href = '/file.txt'; }"),
):
page = await b.new_page(accept_downloads=True)
await page.goto(f"http://127.0.0.1:{port}/")
try:
async with page.expect_download(timeout=8000) as info:
await page.evaluate(js)
dl = await info.value
print(f" {module:11s} {label:28s}: OK {dl.suggested_filename!r}")
except Exception as exc:
print(f" {module:11s} {label:28s}: {type(exc).__name__}: {str(exc)[:60]}")
await page.close()
await b.close()
PORT = serve()
for m in sys.argv[1:] or ["playwright", "rustwright"]:
asyncio.run(run(m, PORT))
Result
playwright window.open(url, '_blank') : OK 'hello.txt'
playwright window.open(url) : OK 'hello.txt'
playwright location.href : OK 'hello.txt'
rustwright window.open(url, '_blank') : TimeoutError: Timeout 8000ms exceeded while waiting for event "download"
rustwright window.open(url) : TimeoutError: Timeout 8000ms exceeded while waiting for event "download"
rustwright location.href : OK 'hello.txt'
Expected: the window.open rows resolve like the location.href row.
Environment
- rustwright 0.1.1 (PyPI) and git
ec130a64 — both reproduce
- playwright 1.61.0 for comparison
- Chromium headless shell 1228, Linux x86_64, Python 3.14
Why it matters
Found while evaluating rustwright as a drop-in for a Playwright suite. The app under test downloads a file by redirecting the browser to a presigned URL through window.open, which is a common pattern for "download this artifact" flows where the URL is generated server-side.
The failure is hard to attribute from the test side: the click lands, the server receives and serves the request (confirmed in the application's own logs), and only the expect_download wait times out — so it reads as a broken download feature rather than a missing event.
Summary
A download triggered by
window.open()never reachespage.expect_download(). Playwright attributes such a download to the opener page and resolves normally; rustwright waits out the timeout.Downloads triggered by a same-tab navigation (
location.href = url), by an<a download>element, or by a blob URL all work correctly — only thewindow.open()path is affected.Repro
Deterministic, local, no network. The server responds to
/file.txtwithContent-Disposition: attachment, so the popup never becomes a page — it becomes a download.Result
Expected: the
window.openrows resolve like thelocation.hrefrow.Environment
ec130a64— both reproduceWhy it matters
Found while evaluating rustwright as a drop-in for a Playwright suite. The app under test downloads a file by redirecting the browser to a presigned URL through
window.open, which is a common pattern for "download this artifact" flows where the URL is generated server-side.The failure is hard to attribute from the test side: the click lands, the server receives and serves the request (confirmed in the application's own logs), and only the
expect_downloadwait times out — so it reads as a broken download feature rather than a missing event.