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

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 

8 

9 

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 ) 

23 

24 def get_all_urls(self): 

25 resolver = get_resolver() 

26 urls = [] 

27 

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) 

77 

78 collect_urls(resolver.url_patterns) 

79 return sorted(urls) 

80 

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)) 

87 

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}") 

93 

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)) 

99 

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 ) 

107 

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 = [] 

113 

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() 

118 

119 # Setup SocialApps in DB (inside async context) 

120 from django.contrib.sites.models import Site 

121 from django.apps import apps 

122 

123 if apps.is_installed("allauth.socialaccount"): 

124 from allauth.socialaccount.models import SocialApp 

125 

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) 

135 

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() 

145 

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/"] 

150 

151 print( 

152 f"\n[{browser_name}] Testing {len(target_urls)} URLs " 

153 f"(Anonymous={is_anonymous})..." 

154 ) 

155 

156 async def check_url(url, browser_name=browser_name): 

157 new_page = await context.new_page() 

158 errors = [] 

159 

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 ) 

169 

170 # Capture unhandled exceptions 

171 new_page.on( 

172 "pageerror", 

173 lambda exc: errors.append(f"JS Exception: {exc}"), 

174 ) 

175 

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 ) 

188 

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}") 

194 

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 ) 

202 

203 new_page.on("response", handle_response) 

204 

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) 

213 

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() 

224 

225 # Run up to 5 concurrent checks 

226 semaphore = asyncio.Semaphore(5) 

227 

228 async def sem_check(url): 

229 async with semaphore: 

230 return await check_url(url) 

231 

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]) 

235 

236 await browser.close() 

237 

238 from django.db import connection 

239 

240 connection.close() 

241 

242 return all_results