@@ -59,11 +59,89 @@ def test_wait_for_services(self):
5959
6060
6161class TestServices (TestUtilities , unittest .TestCase ):
62+ custom_static_token = None
63+
6264 @property
6365 def failureException (self ):
6466 TestServices .failed_test = True
6567 return super ().failureException
6668
69+ @classmethod
70+ def _execute_docker_compose_command (cls , cmd_args , use_text_mode = False ):
71+ """Execute a docker compose command and log output.
72+
73+ Args:
74+ cmd_args: List of command arguments for subprocess.Popen
75+ use_text_mode: If True, use text mode for subprocess output
76+
77+ Returns:
78+ Tuple of (output, error) from command execution
79+ """
80+ kwargs = {
81+ "stdout" : subprocess .PIPE ,
82+ "stderr" : subprocess .PIPE ,
83+ "cwd" : cls .root_location ,
84+ }
85+ if use_text_mode :
86+ kwargs ["text" ] = True
87+ cmd = subprocess .run (cmd_args , check = False , ** kwargs )
88+ if use_text_mode :
89+ output , error = cmd .stdout , cmd .stderr
90+ else :
91+ output = cmd .stdout .decode ("utf-8" , errors = "replace" ) if cmd .stdout else ""
92+ error = cmd .stderr .decode ("utf-8" , errors = "replace" ) if cmd .stderr else ""
93+ output , error = map (str , (cmd .stdout , cmd .stderr ))
94+ with open (cls .config ["logs_file" ], "a" ) as logs_file :
95+ logs_file .write (output )
96+ logs_file .write (error )
97+ if cmd .returncode != 0 :
98+ raise RuntimeError (
99+ f"docker compose command failed "
100+ f"({ cmd .returncode } ): { ' ' .join (cmd_args )} "
101+ )
102+ return output , error
103+
104+ @classmethod
105+ def _setup_admin_theme_links (cls ):
106+ """Configure admin theme links during tests.
107+
108+ The default docker-compose setup does not allow injecting
109+ OPENWISP_ADMIN_THEME_LINKS dynamically, so this method updates
110+ Django settings inside the running container and reloads uWSGI.
111+ This enables the Selenium tests to verify that a custom static CSS
112+ file is served by the admin interface.
113+ """
114+ css_path = os .path .join (
115+ cls .root_location ,
116+ "customization" ,
117+ "theme" ,
118+ cls .config ["custom_css_filename" ],
119+ )
120+ cls .custom_static_token = str (time .time_ns ())
121+ with open (css_path , "w" ) as custom_css_file :
122+ custom_css_file .write (
123+ f"body{{--openwisp-test: { cls .custom_static_token } ;}}"
124+ )
125+ script = rf"""
126+ grep -q OPENWISP_ADMIN_THEME_LINKS /opt/openwisp/openwisp/settings.py || \
127+ printf "\nOPENWISP_ADMIN_THEME_LINKS=[{{\"type\":\"text/css\",\"href\":\"/static/admin/css/openwisp.css\",\"rel\":\"stylesheet\",\"media\":\"all\"}},{{\"type\":\"text/css\",\"href\":\"/static/{ cls .config ["custom_css_filename" ]} \",\"rel\":\"stylesheet\",\"media\":\"all\"}},{{\"type\":\"image/x-icon\",\"href\":\"ui/openwisp/images/favicon.png\",\"rel\":\"icon\"}}]\n" >> /opt/openwisp/openwisp/settings.py &&
128+ python collectstatic.py &&
129+ uwsgi --reload uwsgi.pid
130+ """ # noqa: E501
131+ cls ._execute_docker_compose_command (
132+ [
133+ "docker" ,
134+ "compose" ,
135+ "exec" ,
136+ "-T" ,
137+ "dashboard" ,
138+ "bash" ,
139+ "-c" ,
140+ script ,
141+ ],
142+ use_text_mode = True ,
143+ )
144+
67145 @classmethod
68146 def setUpClass (cls ):
69147 cls .failed_test = False
@@ -76,7 +154,7 @@ def setUpClass(cls):
76154 os .path .dirname (os .path .realpath (__file__ )), "data.py"
77155 )
78156 entrypoint = "python manage.py shell --command='import data; data.setup()'"
79- cmd = subprocess . Popen (
157+ cls . _execute_docker_compose_command (
80158 [
81159 "docker" ,
82160 "compose" ,
@@ -87,22 +165,12 @@ def setUpClass(cls):
87165 "--volume" ,
88166 f"{ test_data_file } :/opt/openwisp/data.py" ,
89167 "dashboard" ,
90- ],
91- universal_newlines = True ,
92- stdout = subprocess .PIPE ,
93- stderr = subprocess .PIPE ,
94- cwd = cls .root_location ,
168+ ]
95169 )
96- output , error = map (str , cmd .communicate ())
97- with open (cls .config ["logs_file" ], "w" ) as logs_file :
98- logs_file .write (output )
99- logs_file .write (error )
100- subprocess .run (
170+ cls ._execute_docker_compose_command (
101171 ["docker" , "compose" , "up" , "--detach" ],
102- stdout = subprocess .DEVNULL ,
103- stderr = subprocess .DEVNULL ,
104- cwd = cls .root_location ,
105172 )
173+ cls ._setup_admin_theme_links ()
106174 # Create base drivers (Firefox)
107175 if cls .config ["driver" ] == "firefox" :
108176 cls .base_driver = cls .get_firefox_webdriver ()
@@ -122,6 +190,15 @@ def tearDownClass(cls):
122190 print (f"Unable to delete resource at: { resource_link } " )
123191 cls .second_driver .quit ()
124192 cls .base_driver .quit ()
193+ # Remove the temporary custom CSS file created for testing
194+ css_path = os .path .join (
195+ cls .root_location ,
196+ "customization" ,
197+ "theme" ,
198+ cls .config ["custom_css_filename" ],
199+ )
200+ if os .path .exists (css_path ):
201+ os .remove (css_path )
125202 if cls .failed_test and cls .config ["logs" ]:
126203 cmd = subprocess .Popen (
127204 ["docker" , "compose" , "logs" ],
@@ -156,6 +233,16 @@ def test_admin_login(self):
156233 )
157234 self .fail (message )
158235
236+ def test_custom_static_files_loaded (self ):
237+ self .login ()
238+ self .open ("/admin/" )
239+ # Check if the custom CSS variable is applied
240+ value = self .web_driver .execute_script (
241+ "return getComputedStyle(document.body)"
242+ ".getPropertyValue('--openwisp-test');"
243+ )
244+ self .assertEqual (value .strip (), self .custom_static_token )
245+
159246 def test_device_monitoring_charts (self ):
160247 self .login ()
161248 self .get_resource ("test-device" , "/admin/config/device/" )
@@ -235,9 +322,17 @@ def test_forgot_password(self):
235322 """Test forgot password to ensure that postfix is working properly."""
236323
237324 self .logout ()
325+ try :
326+ WebDriverWait (self .base_driver , 3 ).until (
327+ EC .text_to_be_present_in_element (
328+ (By .CSS_SELECTOR , ".title-wrapper h1" ), "Logged out"
329+ )
330+ )
331+ except TimeoutException :
332+ self .fail ("Logout failed." )
238333 self .open ("/accounts/password/reset/" )
239334 self .find_element (By .NAME , "email" ).send_keys ("admin@example.com" )
240- self .find_element (By .XPATH , '// button[@ type="submit"]' ).click ()
335+ self .find_element (By .CSS_SELECTOR , 'button[type="submit"]' ).click ()
241336 self ._wait_until_page_ready ()
242337 self .assertIn (
243338 "We have sent you an email. If you have not received "
0 commit comments