Claude commited on
Commit
30d9456
·
unverified ·
1 Parent(s): 5796881

Fix hanging Playwright tests: make the browser fixture function-scoped

Browse files

The session-scoped `browser` fixture hung indefinitely on the first test
that used it. Root cause: pytest-asyncio runs each test on its own event
loop by default, but the fixture was set up once against whichever loop
was active on first use. Playwright's connection keeps a background
reader task bound to that original loop, so a later test's awaits (on a
different loop) don't error - they just wait forever for a reply the old
loop's task can never deliver.

Confirmed by reproducing the hang, then ruling out the scraping/fixture
logic itself by running the same call in a plain script outside pytest
(single event loop throughout), where it completed instantly.

Switching to a function-scoped fixture (fresh browser per test, ~1s extra
each) sidesteps the whole cross-loop class of bug instead of fighting
pytest-asyncio's loop-scope configuration. All 67 tests now pass in ~6s.

Files changed (1) hide show
  1. tests/conftest.py +16 -3
tests/conftest.py CHANGED
@@ -28,10 +28,23 @@ def _find_chromium_executable() -> str | None:
28
  return str(candidates[-1]) if candidates else None
29
 
30
 
31
- @pytest_asyncio.fixture(scope="session")
32
  async def browser():
33
- """A single real headless Chromium instance shared by the whole test
34
- session, exactly like the one `app.py`'s lifespan starts in production.
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  """
36
  from playwright.async_api import async_playwright
37
 
 
28
  return str(candidates[-1]) if candidates else None
29
 
30
 
31
+ @pytest_asyncio.fixture
32
  async def browser():
33
+ """A real headless Chromium instance, exactly like the one `app.py`'s
34
+ lifespan starts in production.
35
+
36
+ Function-scoped (one launch per test) rather than session-scoped: each
37
+ pytest-asyncio test runs on its own event loop by default, and a
38
+ session-scoped instance would be created on one loop's setup and then
39
+ awaited from a different, later loop in the next test. Playwright's
40
+ connection keeps a background reader task bound to the loop it was
41
+ created on, so calls from another loop don't error - they just hang
42
+ forever waiting for a reply the old loop's task will never deliver.
43
+ Confirmed by reproducing this exact hang with a session-scoped version
44
+ of this fixture, then ruling out the scraping logic itself by running
45
+ the same call in a plain script outside pytest (single event loop),
46
+ where it completed instantly. A fresh browser per test costs roughly a
47
+ second of extra startup but sidesteps the whole cross-loop class of bug.
48
  """
49
  from playwright.async_api import async_playwright
50