Skip to content

Commit 2a4ed92

Browse files
committed
Improve test runner speed
1 parent 56c05e5 commit 2a4ed92

File tree

13 files changed

+418
-545
lines changed

13 files changed

+418
-545
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ filterwarnings = """
115115
testpaths = "tests"
116116
xfail_strict = true
117117
asyncio_mode = "auto"
118-
asyncio_default_fixture_loop_scope = "function"
118+
asyncio_default_fixture_loop_scope = "session"
119+
asyncio_default_test_loop_scope = "session"
119120
log_cli_level = "INFO"
120121

121122
#######################################

src/reactpy/testing/display.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,33 @@
44
from types import TracebackType
55
from typing import Any
66

7-
from playwright.async_api import (
8-
Browser,
9-
BrowserContext,
10-
Page,
11-
async_playwright,
12-
)
7+
from playwright.async_api import Browser, Page, async_playwright
138

149
from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT
1510
from reactpy.testing.backend import BackendFixture
11+
from reactpy.testing.common import GITHUB_ACTIONS
1612
from reactpy.types import RootComponentConstructor
1713

1814

1915
class DisplayFixture:
2016
"""A fixture for running web-based tests using ``playwright``"""
2117

22-
_exit_stack: AsyncExitStack
18+
page: Page
19+
browser_is_external: bool = False
20+
backend_is_external: bool = False
2321

2422
def __init__(
2523
self,
2624
backend: BackendFixture | None = None,
27-
driver: Browser | BrowserContext | Page | None = None,
25+
browser: Browser | None = None,
2826
) -> None:
29-
if backend is not None:
27+
if backend:
28+
self.backend_is_external = True
3029
self.backend = backend
31-
if driver is not None:
32-
if isinstance(driver, Page):
33-
self.page = driver
34-
else:
35-
self._browser = driver
30+
31+
if browser:
32+
self.browser_is_external = True
33+
self.browser = browser
3634

3735
async def show(
3836
self,
@@ -42,34 +40,42 @@ async def show(
4240
await self.goto("/")
4341

4442
async def goto(self, path: str, query: Any | None = None) -> None:
43+
await self.configure_page()
4544
await self.page.goto(self.backend.url(path, query))
4645

4746
async def __aenter__(self) -> DisplayFixture:
48-
es = self._exit_stack = AsyncExitStack()
49-
50-
browser: Browser | BrowserContext
51-
if not hasattr(self, "page"):
52-
if not hasattr(self, "_browser"):
53-
pw = await es.enter_async_context(async_playwright())
54-
browser = await pw.chromium.launch()
55-
else:
56-
browser = self._browser
57-
self.page = await browser.new_page()
58-
59-
self.page.set_default_timeout(REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000)
60-
self.page.on("console", lambda msg: print(f"BROWSER CONSOLE: {msg.text}")) # noqa: T201
61-
self.page.on("pageerror", lambda exc: print(f"BROWSER ERROR: {exc}")) # noqa: T201
47+
self.browser_exit_stack = AsyncExitStack()
48+
self.backend_exit_stack = AsyncExitStack()
49+
50+
if not hasattr(self, "browser"):
51+
pw = await self.browser_exit_stack.enter_async_context(async_playwright())
52+
self.browser = await pw.chromium.launch(headless=GITHUB_ACTIONS)
53+
await self.configure_page()
54+
6255
if not hasattr(self, "backend"): # nocov
6356
self.backend = BackendFixture()
64-
await es.enter_async_context(self.backend)
57+
await self.backend_exit_stack.enter_async_context(self.backend)
6558

6659
return self
6760

61+
async def configure_page(self) -> None:
62+
"""Hook for configuring the page before use."""
63+
if getattr(self, "page", None) is None:
64+
self.page = await self.browser.new_page()
65+
self.page.set_default_timeout(REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000)
66+
self.page.on("console", lambda msg: print(f"BROWSER CONSOLE: {msg.text}")) # noqa: T201
67+
self.page.on("pageerror", lambda exc: print(f"BROWSER ERROR: {exc}")) # noqa: T201
68+
6869
async def __aexit__(
6970
self,
7071
exc_type: type[BaseException] | None,
7172
exc_value: BaseException | None,
7273
traceback: TracebackType | None,
7374
) -> None:
7475
self.backend.mount(None)
75-
await self._exit_stack.aclose()
76+
if getattr(self, "page", None) is not None:
77+
await self.page.close()
78+
if not self.browser_is_external:
79+
await self.browser_exit_stack.aclose()
80+
if not self.backend_is_external:
81+
await self.backend_exit_stack.aclose()

tests/conftest.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
import subprocess
66

77
import pytest
8-
from _pytest.config import Config
98
from _pytest.config.argparsing import Parser
109

1110
from reactpy.config import (
1211
REACTPY_ASYNC_RENDERING,
1312
REACTPY_DEBUG,
14-
REACTPY_TESTS_DEFAULT_TIMEOUT,
1513
)
1614
from reactpy.testing import (
1715
BackendFixture,
@@ -54,30 +52,20 @@ def rebuild():
5452
subprocess.run(["hatch", "build", "-t", "wheel"], check=True, env=env) # noqa: S607
5553

5654

57-
@pytest.fixture
58-
async def display(server, page):
59-
async with DisplayFixture(server, page) as display:
55+
@pytest.fixture(scope="session")
56+
async def display(server, browser):
57+
async with DisplayFixture(backend=server, browser=browser) as display:
6058
yield display
6159

6260

63-
@pytest.fixture
61+
@pytest.fixture(scope="session")
6462
async def server():
6563
async with BackendFixture() as server:
6664
yield server
6765

6866

69-
@pytest.fixture
70-
async def page(browser):
71-
pg = await browser.new_page()
72-
pg.set_default_timeout(REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000)
73-
try:
74-
yield pg
75-
finally:
76-
await pg.close()
77-
78-
79-
@pytest.fixture
80-
async def browser(pytestconfig: Config):
67+
@pytest.fixture(scope="session")
68+
async def browser(pytestconfig: pytest.Config):
8169
from playwright.async_api import async_playwright
8270

8371
async with async_playwright() as pw:

tests/test_asgi/test_middleware.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from reactpy.testing import BackendFixture, DisplayFixture
1717

1818

19-
@pytest.fixture()
20-
async def display(page):
19+
@pytest.fixture(scope="module")
20+
async def display(browser):
2121
"""Override for the display fixture that uses ReactPyMiddleware."""
2222
templates = Jinja2Templates(
2323
env=JinjaEnvironment(
@@ -32,7 +32,7 @@ async def homepage(request):
3232
app = Starlette(routes=[Route("/", homepage)])
3333

3434
async with BackendFixture(app) as server:
35-
async with DisplayFixture(backend=server, driver=page) as new_display:
35+
async with DisplayFixture(backend=server, browser=browser) as new_display:
3636
yield new_display
3737

3838

@@ -56,7 +56,7 @@ async def app(scope, receive, send):
5656
ReactPyMiddleware(app, root_components=["abc"], web_modules_dir=Path("invalid"))
5757

5858

59-
async def test_unregistered_root_component():
59+
async def test_unregistered_root_component(browser):
6060
templates = Jinja2Templates(
6161
env=JinjaEnvironment(
6262
loader=JinjaFileSystemLoader("tests/templates"),
@@ -75,7 +75,7 @@ def Stub():
7575
app = ReactPyMiddleware(app, root_components=["tests.sample.SampleApp"])
7676

7777
async with BackendFixture(app) as server:
78-
async with DisplayFixture(backend=server) as new_display:
78+
async with DisplayFixture(backend=server, browser=browser) as new_display:
7979
await new_display.show(Stub)
8080

8181
# Wait for the log record to be populated
@@ -106,7 +106,7 @@ def Hello():
106106
await display.page.wait_for_selector("#hello")
107107

108108

109-
async def test_static_file_not_found(page):
109+
async def test_static_file_not_found():
110110
async def app(scope, receive, send): ...
111111

112112
app = ReactPyMiddleware(app, [])
@@ -119,7 +119,7 @@ async def app(scope, receive, send): ...
119119
assert response.status_code == 404
120120

121121

122-
async def test_templatetag_bad_kwargs(page, caplog):
122+
async def test_templatetag_bad_kwargs(caplog, browser):
123123
"""Override for the display fixture that uses ReactPyMiddleware."""
124124
templates = Jinja2Templates(
125125
env=JinjaEnvironment(
@@ -134,7 +134,7 @@ async def homepage(request):
134134
app = Starlette(routes=[Route("/", homepage)])
135135

136136
async with BackendFixture(app) as server:
137-
async with DisplayFixture(backend=server, driver=page) as new_display:
137+
async with DisplayFixture(backend=server, browser=browser) as new_display:
138138
await new_display.goto("/")
139139

140140
# This test could be improved by actually checking if `bad kwargs` error message is shown in

tests/test_asgi/test_pyscript.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@
1313
from reactpy.testing import BackendFixture, DisplayFixture
1414

1515

16-
@pytest.fixture()
17-
async def display(page):
16+
@pytest.fixture(scope="module")
17+
async def display(browser):
1818
"""Override for the display fixture that uses ReactPyMiddleware."""
1919
app = ReactPyCsr(
2020
Path(__file__).parent / "pyscript_components" / "root.py",
2121
initial=html.div({"id": "loading"}, "Loading..."),
2222
)
2323

2424
async with BackendFixture(app) as server:
25-
async with DisplayFixture(backend=server, driver=page) as new_display:
25+
async with DisplayFixture(backend=server, browser=browser) as new_display:
2626
yield new_display
2727

2828

29-
@pytest.fixture()
30-
async def multi_file_display(page):
29+
@pytest.fixture(scope="module")
30+
async def multi_file_display(browser):
3131
"""Override for the display fixture that uses ReactPyMiddleware."""
3232
app = ReactPyCsr(
3333
Path(__file__).parent / "pyscript_components" / "load_first.py",
@@ -36,12 +36,12 @@ async def multi_file_display(page):
3636
)
3737

3838
async with BackendFixture(app) as server:
39-
async with DisplayFixture(backend=server, driver=page) as new_display:
39+
async with DisplayFixture(backend=server, browser=browser) as new_display:
4040
yield new_display
4141

4242

43-
@pytest.fixture()
44-
async def jinja_display(page):
43+
@pytest.fixture(scope="module")
44+
async def jinja_display(browser):
4545
"""Override for the display fixture that uses ReactPyMiddleware."""
4646
templates = Jinja2Templates(
4747
env=JinjaEnvironment(
@@ -56,7 +56,7 @@ async def homepage(request):
5656
app = Starlette(routes=[Route("/", homepage)])
5757

5858
async with BackendFixture(app) as server:
59-
async with DisplayFixture(backend=server, driver=page) as new_display:
59+
async with DisplayFixture(backend=server, browser=browser) as new_display:
6060
yield new_display
6161

6262

tests/test_asgi/test_standalone.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def ShowRoute():
123123
assert hook_val.current is not None
124124

125125

126-
async def test_customized_head(page):
126+
async def test_customized_head(browser):
127127
custom_title = "Custom Title for ReactPy"
128128

129129
@reactpy.component
@@ -133,12 +133,12 @@ def sample():
133133
app = ReactPy(sample, html_head=html.head(html.title(custom_title)))
134134

135135
async with BackendFixture(app) as server:
136-
async with DisplayFixture(backend=server, driver=page) as new_display:
136+
async with DisplayFixture(backend=server, browser=browser) as new_display:
137137
await new_display.show(sample)
138138
assert (await new_display.page.title()) == custom_title
139139

140140

141-
async def test_head_request(page):
141+
async def test_head_request():
142142
@reactpy.component
143143
def sample():
144144
return html.h1("Hello World")

tests/test_client.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import asyncio
22
from pathlib import Path
33

4-
from playwright.async_api import Page
5-
64
import reactpy
75
from reactpy.testing import BackendFixture, DisplayFixture, poll
86
from tests.tooling.common import DEFAULT_TYPE_DELAY
@@ -11,9 +9,7 @@
119
JS_DIR = Path(__file__).parent / "js"
1210

1311

14-
async def test_automatic_reconnect(
15-
display: DisplayFixture, page: Page, server: BackendFixture
16-
):
12+
async def test_automatic_reconnect(display: DisplayFixture, server: BackendFixture):
1713
@reactpy.component
1814
def SomeComponent():
1915
count, incr_count = use_counter(0)
@@ -26,35 +22,35 @@ def SomeComponent():
2622

2723
async def get_count():
2824
# need to refetch element because may unmount on reconnect
29-
count = await page.wait_for_selector("#count")
25+
count = await display.page.wait_for_selector("#count")
3026
return await count.get_attribute("data-count")
3127

3228
await display.show(SomeComponent)
3329

3430
await poll(get_count).until_equals("0")
35-
incr = await page.wait_for_selector("#incr")
31+
incr = await display.page.wait_for_selector("#incr")
3632
await incr.click()
3733

3834
await poll(get_count).until_equals("1")
39-
incr = await page.wait_for_selector("#incr")
35+
incr = await display.page.wait_for_selector("#incr")
4036
await incr.click()
4137

4238
await poll(get_count).until_equals("2")
43-
incr = await page.wait_for_selector("#incr")
39+
incr = await display.page.wait_for_selector("#incr")
4440
await incr.click()
4541

4642
await server.restart()
4743

4844
await poll(get_count).until_equals("0")
49-
incr = await page.wait_for_selector("#incr")
45+
incr = await display.page.wait_for_selector("#incr")
5046
await incr.click()
5147

5248
await poll(get_count).until_equals("1")
53-
incr = await page.wait_for_selector("#incr")
49+
incr = await display.page.wait_for_selector("#incr")
5450
await incr.click()
5551

5652
await poll(get_count).until_equals("2")
57-
incr = await page.wait_for_selector("#incr")
53+
incr = await display.page.wait_for_selector("#incr")
5854
await incr.click()
5955

6056

tests/test_pyscript/test_components.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from reactpy.testing.backend import root_hotswap_component
1010

1111

12-
@pytest.fixture()
13-
async def display(page):
12+
@pytest.fixture(scope="module")
13+
async def display(browser):
1414
"""Override for the display fixture that uses ReactPyMiddleware."""
1515
app = ReactPy(root_hotswap_component, pyscript_setup=True)
1616

1717
async with BackendFixture(app) as server:
18-
async with DisplayFixture(backend=server, driver=page) as new_display:
18+
async with DisplayFixture(backend=server, browser=browser) as new_display:
1919
yield new_display
2020

2121

0 commit comments

Comments
 (0)