2
2
import asyncio
3
3
import os
4
4
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
7
7
8
8
import decorator
9
+ from channels .routing import get_default_application
9
10
from channels .testing import ChannelsLiveServerTestCase
10
- from channels .testing .live import make_application
11
11
from django .core .exceptions import ImproperlyConfigured
12
12
from django .core .management import call_command
13
13
from django .db import connections
16
16
17
17
from reactpy_django .utils import str_to_bool
18
18
19
+ if TYPE_CHECKING :
20
+ from daphne .testing import DaphneProcess
21
+
19
22
GITHUB_ACTIONS = os .getenv ("GITHUB_ACTIONS" , "False" )
20
23
21
24
22
25
class PlaywrightTestCase (ChannelsLiveServerTestCase ):
23
- from reactpy_django import config
24
-
25
26
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
+ ####################################################
27
40
@classmethod
28
41
def setUpClass (cls ):
29
42
# Repurposed from ChannelsLiveServerTestCase._pre_setup
30
43
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 ( ):
32
45
msg = "ChannelLiveServerTestCase can not be used with in memory databases"
33
46
raise ImproperlyConfigured (msg )
34
47
cls ._live_server_modified_settings = modify_settings (ALLOWED_HOSTS = {"append" : cls .host })
35
48
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 })
41
59
42
60
# 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 ):
43
110
if sys .platform == "win32" :
44
111
asyncio .set_event_loop_policy (asyncio .WindowsProactorEventLoopPolicy ())
45
112
cls .playwright = sync_playwright ().start ()
@@ -49,26 +116,13 @@ def setUpClass(cls):
49
116
cls .page .set_default_timeout (10000 )
50
117
51
118
@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 ()
63
121
cls .playwright .stop ()
64
122
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 :
72
126
call_command (
73
127
"flush" ,
74
128
verbosity = 0 ,
@@ -77,26 +131,16 @@ def tearDownClass(cls):
77
131
reset_sequences = False ,
78
132
)
79
133
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`."""
92
134
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."""
93
137
94
- def navigate_to_page (path : str ):
95
138
def _decorator (func : Callable ):
96
139
@decorator .decorator
97
140
def _wrapper (func : Callable , self : PlaywrightTestCase , * args , ** kwargs ):
98
141
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 ('/' )} " )
100
144
return func (self , * args , ** kwargs )
101
145
102
146
return _wrapper (func )
0 commit comments