Skip to content

Commit 9f9f99d

Browse files
committed
Refactor PlaywrightTestCase
1 parent c8d6cf9 commit 9f9f99d

File tree

2 files changed

+102
-72
lines changed

2 files changed

+102
-72
lines changed

tests/test_app/tests/test_components.py

+14-28
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class GenericComponentTests(PlaywrightTestCase):
2121
@classmethod
2222
def setUpClass(cls):
2323
super().setUpClass()
24-
cls.page.goto(f"http://{cls.host}:{cls._port}")
24+
cls.page.goto(f"http://{cls.host}:{cls._port_0}")
2525

2626
def test_hello_world(self):
2727
self.page.wait_for_selector("#hello-world")
@@ -288,7 +288,7 @@ class PrerenderTests(PlaywrightTestCase):
288288
@classmethod
289289
def setUpClass(cls):
290290
super().setUpClass()
291-
cls.page.goto(f"http://{cls.host}:{cls._port}/prerender/")
291+
cls.page.goto(f"http://{cls.host}:{cls._port_0}/prerender/")
292292

293293
def test_prerender(self):
294294
"""Verify if round-robin host selection is working."""
@@ -326,7 +326,7 @@ class ErrorTests(PlaywrightTestCase):
326326
@classmethod
327327
def setUpClass(cls):
328328
super().setUpClass()
329-
cls.page.goto(f"http://{cls.host}:{cls._port}/errors/")
329+
cls.page.goto(f"http://{cls.host}:{cls._port_0}/errors/")
330330

331331
def test_component_does_not_exist_error(self):
332332
broken_component = self.page.locator("#component_does_not_exist_error")
@@ -435,7 +435,7 @@ class ChannelLayersTests(PlaywrightTestCase):
435435
@classmethod
436436
def setUpClass(cls):
437437
super().setUpClass()
438-
cls.page.goto(f"http://{cls.host}:{cls._port}/channel-layers/")
438+
cls.page.goto(f"http://{cls.host}:{cls._port_0}/channel-layers/")
439439

440440
def test_channel_layer_components(self):
441441
sender = self.page.wait_for_selector("#sender")
@@ -459,7 +459,7 @@ class PyscriptTests(PlaywrightTestCase):
459459
@classmethod
460460
def setUpClass(cls):
461461
super().setUpClass()
462-
cls.page.goto(f"http://{cls.host}:{cls._port}/pyscript/")
462+
cls.page.goto(f"http://{cls.host}:{cls._port_0}/pyscript/")
463463

464464
def test_0_hello_world(self):
465465
self.page.wait_for_selector("#hello-world-loading")
@@ -510,23 +510,9 @@ def test_1_javascript_module_execution_within_pyscript(self):
510510

511511

512512
class DistributedComputingTests(PlaywrightTestCase):
513-
@classmethod
514-
def setUpServer(cls):
515-
super().setUpServer()
516-
cls._server_process2 = cls.ProtocolServerProcess(cls.host, cls.get_application)
517-
cls._server_process2.start()
518-
cls._server_process2.ready.wait()
519-
cls._port2 = cls._server_process2.port.value
520-
521-
@classmethod
522-
def tearDownServer(cls):
523-
super().tearDownServer()
524-
cls._server_process2.terminate()
525-
cls._server_process2.join()
526-
527513
def test_host_roundrobin(self):
528514
"""Verify if round-robin host selection is working."""
529-
self.page.goto(f"{self.live_server_url}/roundrobin/{self._port}/{self._port2}/8")
515+
self.page.goto(f"{self.live_server_url}/roundrobin/{self._port_0}/{self._port_1}/8")
530516
elem0 = self.page.locator(".custom_host-0")
531517
elem1 = self.page.locator(".custom_host-1")
532518
elem2 = self.page.locator(".custom_host-2")
@@ -544,8 +530,8 @@ def test_host_roundrobin(self):
544530
elem3.get_attribute("data-port"),
545531
}
546532
correct_ports = {
547-
str(self._port),
548-
str(self._port2),
533+
str(self._port_0),
534+
str(self._port_1),
549535
}
550536

551537
# There should only be two ports in the set
@@ -554,15 +540,15 @@ def test_host_roundrobin(self):
554540

555541
def test_custom_host(self):
556542
"""Make sure that the component is rendered by a separate server."""
557-
self.page.goto(f"{self.live_server_url}/port/{self._port2}/")
543+
self.page.goto(f"{self.live_server_url}/port/{self._port_1}/")
558544
elem = self.page.locator(".custom_host-0")
559545
elem.wait_for()
560-
assert f"Server Port: {self._port2}" in elem.text_content()
546+
assert f"Server Port: {self._port_1}" in elem.text_content()
561547

562548
def test_custom_host_wrong_port(self):
563549
"""Make sure that other ports are not rendering components."""
564550
tmp_sock = socket.socket()
565-
tmp_sock.bind((self._server_process.host, 0))
551+
tmp_sock.bind((self._server_process_0.host, 0))
566552
random_port = tmp_sock.getsockname()[1]
567553
self.page.goto(f"{self.live_server_url}/port/{random_port}/")
568554
with pytest.raises(TimeoutError):
@@ -573,13 +559,13 @@ class OfflineTests(PlaywrightTestCase):
573559
@classmethod
574560
def setUpClass(cls):
575561
super().setUpClass()
576-
cls.page.goto(f"http://{cls.host}:{cls._port}/offline/")
562+
cls.page.goto(f"http://{cls.host}:{cls._port_0}/offline/")
577563

578564
def test_offline_components(self):
579565
self.page.wait_for_selector("div:not([hidden]) > #online")
580566
assert self.page.query_selector("div[hidden] > #offline") is not None
581-
self._server_process.terminate()
582-
self._server_process.join()
567+
self._server_process_0.terminate()
568+
self._server_process_0.join()
583569
self.page.wait_for_selector("div:not([hidden]) > #offline")
584570
assert self.page.query_selector("div[hidden] > #online") is not None
585571

tests/test_app/tests/utils.py

+88-44
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
import asyncio
33
import os
44
import sys
5-
from functools import partial
6-
from typing import Callable
5+
from collections.abc import Iterable
6+
from typing import TYPE_CHECKING, Any, Callable
77

88
import decorator
9+
from channels.routing import get_default_application
910
from channels.testing import ChannelsLiveServerTestCase
10-
from channels.testing.live import make_application
1111
from django.core.exceptions import ImproperlyConfigured
1212
from django.core.management import call_command
1313
from django.db import connections
@@ -16,30 +16,97 @@
1616

1717
from reactpy_django.utils import str_to_bool
1818

19+
if TYPE_CHECKING:
20+
from daphne.testing import DaphneProcess
21+
1922
GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False")
2023

2124

2225
class PlaywrightTestCase(ChannelsLiveServerTestCase):
23-
from reactpy_django import config
24-
2526
databases = {"default"}
26-
27+
total_servers = 3
28+
_server_process_0: "DaphneProcess"
29+
_server_process_1: "DaphneProcess"
30+
_server_process_2: "DaphneProcess"
31+
_server_process_3: "DaphneProcess"
32+
_port_0: int
33+
_port_1: int
34+
_port_2: int
35+
_port_3: int
36+
37+
####################################################
38+
# Overrides for ChannelsLiveServerTestCase methods #
39+
####################################################
2740
@classmethod
2841
def setUpClass(cls):
2942
# Repurposed from ChannelsLiveServerTestCase._pre_setup
3043
for connection in connections.all():
31-
if cls._is_in_memory_db(cls, connection):
44+
if connection.vendor == "sqlite" and connection.is_in_memory_db():
3245
msg = "ChannelLiveServerTestCase can not be used with in memory databases"
3346
raise ImproperlyConfigured(msg)
3447
cls._live_server_modified_settings = modify_settings(ALLOWED_HOSTS={"append": cls.host})
3548
cls._live_server_modified_settings.enable()
36-
cls.get_application = partial(
37-
make_application,
38-
static_wrapper=cls.static_wrapper if cls.serve_static else None,
39-
)
40-
cls.setUpServer()
49+
cls.get_application = get_default_application
50+
51+
# Start the Django webserver(s)
52+
for i in range(cls.total_servers):
53+
cls.start_django_webserver(i)
54+
55+
# Wipe the databases
56+
from reactpy_django import config
57+
58+
cls.flush_databases({"default", config.REACTPY_DATABASE})
4159

4260
# Open a Playwright browser window
61+
cls.start_playwright_client()
62+
63+
@classmethod
64+
def tearDownClass(cls):
65+
# Close the Playwright browser
66+
cls.shutdown_playwright_client()
67+
68+
# Shutdown the Django webserver
69+
for i in range(cls.total_servers):
70+
cls.shutdown_django_webserver(i)
71+
cls._live_server_modified_settings.disable()
72+
73+
# Wipe the databases
74+
from reactpy_django import config
75+
76+
cls.flush_databases({"default", config.REACTPY_DATABASE})
77+
78+
def _pre_setup(self):
79+
"""Handled manually in `setUpClass` to speed things up."""
80+
81+
def _post_teardown(self):
82+
"""Handled manually in `tearDownClass` to prevent TransactionTestCase from doing
83+
database flushing in between tests. This also fixes a `SynchronousOnlyOperation` caused
84+
by a bug within `ChannelsLiveServerTestCase`."""
85+
86+
@property
87+
def live_server_url(self):
88+
"""Provides the URL to the FIRST SPAWNED Django webserver."""
89+
return f"http://{self.host}:{self._port_0}"
90+
91+
#########################
92+
# Custom helper methods #
93+
#########################
94+
@classmethod
95+
def start_django_webserver(cls, num=0):
96+
setattr(cls, f"_server_process_{num}", cls.ProtocolServerProcess(cls.host, cls.get_application))
97+
server_process: DaphneProcess = getattr(cls, f"_server_process_{num}")
98+
server_process.start()
99+
server_process.ready.wait()
100+
setattr(cls, f"_port_{num}", server_process.port.value)
101+
102+
@classmethod
103+
def shutdown_django_webserver(cls, num=0):
104+
server_process: DaphneProcess = getattr(cls, f"_server_process_{num}")
105+
server_process.terminate()
106+
server_process.join()
107+
108+
@classmethod
109+
def start_playwright_client(cls):
43110
if sys.platform == "win32":
44111
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
45112
cls.playwright = sync_playwright().start()
@@ -49,26 +116,13 @@ def setUpClass(cls):
49116
cls.page.set_default_timeout(10000)
50117

51118
@classmethod
52-
def setUpServer(cls):
53-
cls._server_process = cls.ProtocolServerProcess(cls.host, cls.get_application)
54-
cls._server_process.start()
55-
cls._server_process.ready.wait()
56-
cls._port = cls._server_process.port.value
57-
58-
@classmethod
59-
def tearDownClass(cls):
60-
from reactpy_django import config
61-
62-
# Close the Playwright browser
119+
def shutdown_playwright_client(cls):
120+
cls.browser.close()
63121
cls.playwright.stop()
64122

65-
# Close the other server processes
66-
cls.tearDownServer()
67-
68-
# Repurposed from ChannelsLiveServerTestCase._post_teardown
69-
cls._live_server_modified_settings.disable()
70-
# Using set to prevent duplicates
71-
for db_name in {"default", config.REACTPY_DATABASE}: # noqa: PLC0208
123+
@staticmethod
124+
def flush_databases(db_names: Iterable[Any]):
125+
for db_name in db_names:
72126
call_command(
73127
"flush",
74128
verbosity=0,
@@ -77,26 +131,16 @@ def tearDownClass(cls):
77131
reset_sequences=False,
78132
)
79133

80-
@classmethod
81-
def tearDownServer(cls):
82-
cls._server_process.terminate()
83-
cls._server_process.join()
84-
85-
def _pre_setup(self):
86-
"""Handled manually in `setUpClass` to speed things up."""
87-
88-
def _post_teardown(self):
89-
"""Handled manually in `tearDownClass` to prevent TransactionTestCase from doing
90-
database flushing. This is needed to prevent a `SynchronousOnlyOperation` from
91-
occurring due to a bug within `ChannelsLiveServerTestCase`."""
92134

135+
def navigate_to_page(path: str, *, server_num=0):
136+
"""Decorator to make sure the browser is on a specific page before running a test."""
93137

94-
def navigate_to_page(path: str):
95138
def _decorator(func: Callable):
96139
@decorator.decorator
97140
def _wrapper(func: Callable, self: PlaywrightTestCase, *args, **kwargs):
98141
if self.page.url != path:
99-
self.page.goto(f"http://{self.host}:{self._port}/{path.lstrip('/')}")
142+
_port = getattr(self, f"_port_{server_num}")
143+
self.page.goto(f"http://{self.host}:{_port}/{path.lstrip('/')}")
100144
return func(self, *args, **kwargs)
101145

102146
return _wrapper(func)

0 commit comments

Comments
 (0)