Coverage for sm / test_browser.py: 0%
107 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 13:46 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 13:46 +0000
1import os
2import asyncio
3from django.contrib.staticfiles.testing import StaticLiveServerTestCase
4from django.urls import get_resolver
5from django.contrib.auth import get_user_model
6from django.test import tag
7from playwright.async_api import async_playwright
10@tag("browser")
11class BrowserIntegrationTest(StaticLiveServerTestCase):
12 @classmethod
13 def setUpClass(cls):
14 os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
15 super().setUpClass()
16 # Create a superuser for testing all pages
17 User = get_user_model()
18 cls.username = "testadmin"
19 cls.password = "testpass123"
20 cls.user = User.objects.create_superuser(
21 cls.username, "admin@example.com", cls.password
22 )
24 def get_all_urls(self):
25 resolver = get_resolver()
26 urls = []
28 def collect_urls(patterns, prefix=""):
29 for pattern in patterns:
30 if hasattr(pattern, "url_patterns"):
31 collect_urls(pattern.url_patterns, prefix + str(pattern.pattern))
32 else:
33 url = prefix + str(pattern.pattern)
34 url = url.replace("^", "").replace("$", "")
35 if any(
36 skip in url
37 for skip in [
38 "admin",
39 "logout",
40 "debug",
41 "__",
42 "password",
43 "delete",
44 "detail",
45 "schema",
46 "api",
47 "accounts",
48 "2fa",
49 "social",
50 "server",
51 "group/filter",
52 "terms",
53 "privacy",
54 "impressum",
55 "avatar",
56 "history",
57 "search",
58 "cluster",
59 "vendor",
60 "status",
61 "location",
62 "domain",
63 "patchtime",
64 "operatingsystem",
65 "servermodel",
66 "clustersoftware",
67 "clusterpackage",
68 "clusterpackagetype",
69 ]
70 ):
71 continue
72 if "<" in url or "(" in url:
73 continue
74 if not url.startswith("/"):
75 url = "/" + url
76 if url not in urls:
77 urls.append(url)
79 collect_urls(resolver.url_patterns)
80 return sorted(urls)
82 def test_js_integrity_anonymous(self):
83 """
84 Test public pages as an anonymous user to ensure login snippets etc are
85 OK.
86 """
87 results = asyncio.run(self._async_test_js(is_anonymous=True))
89 if results:
90 errors_msg = "\n\n".join(
91 [f"{url}:\n" + "\n".join(errors) for url, errors in results]
92 )
93 self.fail(f"JS/Resource errors found for anonymous user:\n\n{errors_msg}")
95 def test_js_integrity_authenticated(self):
96 """
97 Test project pages as an authenticated user.
98 """
99 results = asyncio.run(self._async_test_js(is_anonymous=False))
101 if results:
102 errors_msg = "\n\n".join(
103 [f"{url}:\n" + "\n".join(errors) for url, errors in results]
104 )
105 self.fail(
106 f"JS/Resource errors found for authenticated user:\n\n{errors_msg}"
107 )
109 async def _async_test_js(self, is_anonymous=False):
110 async with async_playwright() as p:
111 # Test in multiple browsers
112 browser_types = [p.chromium, p.firefox]
113 all_results = []
115 for browser_type in browser_types:
116 browser_name = browser_type.name
117 browser = await browser_type.launch(headless=True)
118 context = await browser.new_context()
120 # Setup SocialApps in DB (inside async context)
121 from django.contrib.sites.models import Site
122 from django.apps import apps
124 if apps.is_installed("allauth.socialaccount"):
125 from allauth.socialaccount.models import SocialApp
127 site = await asyncio.to_thread(Site.objects.get_current)
128 for p_id in ["facebook", "google"]:
129 app, _ = await asyncio.to_thread(
130 SocialApp.objects.get_or_create,
131 provider=p_id,
132 name=p_id.title(),
133 defaults={"client_id": "123", "secret": "abc"},
134 )
135 await asyncio.to_thread(app.sites.add, site)
137 if not is_anonymous:
138 # Login
139 page = await context.new_page()
140 await page.goto(f"{self.live_server_url}/accounts/login/")
141 await page.fill('input[name="login"]', self.username)
142 await page.fill('input[name="password"]', self.password)
143 await page.click('button[type="submit"]')
144 await page.wait_for_load_state("networkidle")
145 await page.close()
147 target_urls = self.get_all_urls()
148 if is_anonymous:
149 # For anonymous, only test root and login-related pages
150 target_urls = ["/", "/accounts/login/"]
152 print(
153 f"\n[{browser_name}] Testing {len(target_urls)} URLs "
154 f"(Anonymous={is_anonymous})..."
155 )
157 async def check_url(url, browser_name=browser_name):
158 new_page = await context.new_page()
159 errors = []
161 # Capture ALL console messages (errors AND warnings)
162 new_page.on(
163 "console",
164 lambda msg: (
165 errors.append(f"Console {msg.type.upper()}: {msg.text}")
166 if msg.type in ["error", "warning"]
167 else None
168 ),
169 )
171 # Capture unhandled exceptions
172 new_page.on(
173 "pageerror",
174 lambda exc: errors.append(f"JS Exception: {exc}"),
175 )
177 # Capture failed network requests
178 new_page.on(
179 "requestfailed",
180 lambda req: errors.append(
181 f"Network Failure ({req.method}): {req.url} - "
182 + (
183 getattr(req.failure, "error_text", req.failure)
184 if req.failure
185 else "Unknown Error"
186 )
187 ),
188 )
190 # Capture non-OK responses and MIME mismatches
191 async def handle_response(res):
192 # 3xx are fine (redirects), but 4xx and 5xx are errors
193 if res.status >= 400:
194 errors.append(f"HTTP {res.status} on {res.url}")
196 # Check for MIME type conflicts on scripts
197 content_type = res.headers.get("content-type", "")
198 if res.ok and ".js" in res.url and "text/html" in content_type:
199 errors.append(
200 f"MIME Type Conflict: {res.url} returned "
201 f"{content_type} (expected javascript)"
202 )
204 new_page.on("response", handle_response)
206 try:
207 await new_page.goto(
208 f"{self.live_server_url}{url}",
209 wait_until="networkidle",
210 timeout=15000,
211 )
212 # Explicit wait for any late-firing JS
213 await asyncio.sleep(1.0)
215 if errors:
216 return (f"[{browser_name}] {url}", errors)
217 return None
218 except Exception as e:
219 return (
220 f"[{browser_name}] {url}",
221 [f"Navigation Error: {str(e)}"],
222 )
223 finally:
224 await new_page.close()
226 # Run up to 5 concurrent checks
227 semaphore = asyncio.Semaphore(5)
229 async def sem_check(url):
230 async with semaphore:
231 return await check_url(url)
233 tasks = [sem_check(url) for url in target_urls]
234 results = await asyncio.gather(*tasks)
235 all_results.extend([r for r in results if r])
237 await browser.close()
239 from django.db import connection
241 connection.close()
243 return all_results