Coverage for sm / test_browser.py: 0%
107 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-24 12:43 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-24 12:43 +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 "terms",
52 "privacy",
53 "impressum",
54 "avatar",
55 "history",
56 "search",
57 "cluster",
58 "vendor",
59 "status",
60 "location",
61 "domain",
62 "patchtime",
63 "operatingsystem",
64 "servermodel",
65 "clustersoftware",
66 "clusterpackage",
67 "clusterpackagetype",
68 ]
69 ):
70 continue
71 if "<" in url or "(" in url:
72 continue
73 if not url.startswith("/"):
74 url = "/" + url
75 if url not in urls:
76 urls.append(url)
78 collect_urls(resolver.url_patterns)
79 return sorted(urls)
81 def test_js_integrity_anonymous(self):
82 """
83 Test public pages as an anonymous user to ensure login snippets etc are
84 OK.
85 """
86 results = asyncio.run(self._async_test_js(is_anonymous=True))
88 if results:
89 errors_msg = "\n\n".join(
90 [f"{url}:\n" + "\n".join(errors) for url, errors in results]
91 )
92 self.fail(f"JS/Resource errors found for anonymous user:\n\n{errors_msg}")
94 def test_js_integrity_authenticated(self):
95 """
96 Test project pages as an authenticated user.
97 """
98 results = asyncio.run(self._async_test_js(is_anonymous=False))
100 if results:
101 errors_msg = "\n\n".join(
102 [f"{url}:\n" + "\n".join(errors) for url, errors in results]
103 )
104 self.fail(
105 f"JS/Resource errors found for authenticated user:\n\n{errors_msg}"
106 )
108 async def _async_test_js(self, is_anonymous=False):
109 async with async_playwright() as p:
110 # Test in multiple browsers
111 browser_types = [p.chromium, p.firefox]
112 all_results = []
114 for browser_type in browser_types:
115 browser_name = browser_type.name
116 browser = await browser_type.launch(headless=True)
117 context = await browser.new_context()
119 # Setup SocialApps in DB (inside async context)
120 from django.contrib.sites.models import Site
121 from django.apps import apps
123 if apps.is_installed("allauth.socialaccount"):
124 from allauth.socialaccount.models import SocialApp
126 site = await asyncio.to_thread(Site.objects.get_current)
127 for p_id in ["facebook", "google"]:
128 app, _ = await asyncio.to_thread(
129 SocialApp.objects.get_or_create,
130 provider=p_id,
131 name=p_id.title(),
132 defaults={"client_id": "123", "secret": "abc"},
133 )
134 await asyncio.to_thread(app.sites.add, site)
136 if not is_anonymous:
137 # Login
138 page = await context.new_page()
139 await page.goto(f"{self.live_server_url}/accounts/login/")
140 await page.fill('input[name="login"]', self.username)
141 await page.fill('input[name="password"]', self.password)
142 await page.click('button[type="submit"]')
143 await page.wait_for_load_state("networkidle")
144 await page.close()
146 target_urls = self.get_all_urls()
147 if is_anonymous:
148 # For anonymous, only test root and login-related pages
149 target_urls = ["/", "/accounts/login/"]
151 print(
152 f"\n[{browser_name}] Testing {len(target_urls)} URLs "
153 f"(Anonymous={is_anonymous})..."
154 )
156 async def check_url(url, browser_name=browser_name):
157 new_page = await context.new_page()
158 errors = []
160 # Capture ALL console messages (errors AND warnings)
161 new_page.on(
162 "console",
163 lambda msg: (
164 errors.append(f"Console {msg.type.upper()}: {msg.text}")
165 if msg.type in ["error", "warning"]
166 else None
167 ),
168 )
170 # Capture unhandled exceptions
171 new_page.on(
172 "pageerror",
173 lambda exc: errors.append(f"JS Exception: {exc}"),
174 )
176 # Capture failed network requests
177 new_page.on(
178 "requestfailed",
179 lambda req: errors.append(
180 f"Network Failure ({req.method}): {req.url} - "
181 + (
182 getattr(req.failure, "error_text", req.failure)
183 if req.failure
184 else "Unknown Error"
185 )
186 ),
187 )
189 # Capture non-OK responses and MIME mismatches
190 async def handle_response(res):
191 # 3xx are fine (redirects), but 4xx and 5xx are errors
192 if res.status >= 400:
193 errors.append(f"HTTP {res.status} on {res.url}")
195 # Check for MIME type conflicts on scripts
196 content_type = res.headers.get("content-type", "")
197 if res.ok and ".js" in res.url and "text/html" in content_type:
198 errors.append(
199 f"MIME Type Conflict: {res.url} returned "
200 f"{content_type} (expected javascript)"
201 )
203 new_page.on("response", handle_response)
205 try:
206 await new_page.goto(
207 f"{self.live_server_url}{url}",
208 wait_until="networkidle",
209 timeout=15000,
210 )
211 # Explicit wait for any late-firing JS
212 await asyncio.sleep(1.0)
214 if errors:
215 return (f"[{browser_name}] {url}", errors)
216 return None
217 except Exception as e:
218 return (
219 f"[{browser_name}] {url}",
220 [f"Navigation Error: {str(e)}"],
221 )
222 finally:
223 await new_page.close()
225 # Run up to 5 concurrent checks
226 semaphore = asyncio.Semaphore(5)
228 async def sem_check(url):
229 async with semaphore:
230 return await check_url(url)
232 tasks = [sem_check(url) for url in target_urls]
233 results = await asyncio.gather(*tasks)
234 all_results.extend([r for r in results if r])
236 await browser.close()
238 from django.db import connection
240 connection.close()
242 return all_results