22import asyncio
33import os
44import 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
88import decorator
9+ from channels .routing import get_default_application
910from channels .testing import ChannelsLiveServerTestCase
10- from channels .testing .live import make_application
1111from django .core .exceptions import ImproperlyConfigured
1212from django .core .management import call_command
1313from django .db import connections
1616
1717from reactpy_django .utils import str_to_bool
1818
19+ if TYPE_CHECKING :
20+ from daphne .testing import DaphneProcess
21+
1922GITHUB_ACTIONS = os .getenv ("GITHUB_ACTIONS" , "False" )
2023
2124
2225class 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