Wanderhalleylee commited on
Commit
a9cff6b
Β·
verified Β·
1 Parent(s): 816ac28

Upload 2 files

Browse files
Files changed (2) hide show
  1. utils/backup.py +354 -0
  2. utils/error_checker.py +1106 -0
utils/backup.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import zipfile
5
+ import requests
6
+ import hashlib
7
+ import time
8
+ from io import BytesIO
9
+ from urllib.parse import urljoin, urlparse, unquote
10
+ from bs4 import BeautifulSoup
11
+ from selenium.webdriver.common.by import By
12
+
13
+
14
+ class SiteBackup:
15
+ """Classe responsΓ‘vel por fazer backup completo de um site aberto no Selenium."""
16
+
17
+ def __init__(self, driver, url):
18
+ self.driver = driver
19
+ self.url = url
20
+ self.base_url = self._get_base_url(url)
21
+ self.domain = urlparse(url).netloc
22
+ self.downloaded_assets = {}
23
+ self.asset_counter = 0
24
+ self.errors = []
25
+
26
+ def _get_base_url(self, url):
27
+ parsed = urlparse(url)
28
+ return f"{parsed.scheme}://{parsed.netloc}"
29
+
30
+ def _safe_filename(self, url, extension=None):
31
+ """Gera um nome de arquivo seguro a partir de uma URL."""
32
+ self.asset_counter += 1
33
+ parsed = urlparse(url)
34
+ path = unquote(parsed.path).strip("/")
35
+
36
+ if path:
37
+ name = path.replace("/", "_").replace("\\", "_")
38
+ name = re.sub(r'[<>:"|?*]', '_', name)
39
+ else:
40
+ name = f"asset_{self.asset_counter}"
41
+
42
+ if extension and not name.endswith(extension):
43
+ name = f"{name}{extension}"
44
+
45
+ if len(name) > 150:
46
+ hash_str = hashlib.md5(url.encode()).hexdigest()[:8]
47
+ ext = os.path.splitext(name)[1] or (extension or "")
48
+ name = f"{hash_str}{ext}"
49
+
50
+ return name
51
+
52
+ def _download_asset(self, url):
53
+ """Baixa um asset e retorna seus bytes."""
54
+ if url in self.downloaded_assets:
55
+ return self.downloaded_assets[url]
56
+
57
+ try:
58
+ headers = {
59
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
60
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
61
+ "Chrome/120.0.0.0 Safari/537.36"
62
+ }
63
+ response = requests.get(url, headers=headers, timeout=15, verify=False)
64
+ if response.status_code == 200:
65
+ self.downloaded_assets[url] = response.content
66
+ return response.content
67
+ except Exception as e:
68
+ self.errors.append(f"Erro ao baixar {url}: {str(e)}")
69
+
70
+ return None
71
+
72
+ def _classify_asset(self, url, content_type=None):
73
+ """Classifica o asset em uma pasta baseado no tipo."""
74
+ url_lower = url.lower()
75
+
76
+ if any(ext in url_lower for ext in ['.css']):
77
+ return "css"
78
+ elif any(ext in url_lower for ext in ['.js', '.mjs']):
79
+ return "js"
80
+ elif any(ext in url_lower for ext in ['.png', '.jpg', '.jpeg', '.gif',
81
+ '.svg', '.webp', '.ico', '.bmp',
82
+ '.avif']):
83
+ return "images"
84
+ elif any(ext in url_lower for ext in ['.woff', '.woff2', '.ttf',
85
+ '.eot', '.otf']):
86
+ return "fonts"
87
+ elif any(ext in url_lower for ext in ['.mp4', '.webm', '.ogg',
88
+ '.mp3', '.wav']):
89
+ return "media"
90
+ elif any(ext in url_lower for ext in ['.json', '.xml', '.csv']):
91
+ return "data"
92
+ else:
93
+ return "assets"
94
+
95
+ def capture_full_html(self):
96
+ """Captura o HTML completo renderizado pelo navegador."""
97
+ try:
98
+ html = self.driver.execute_script("return document.documentElement.outerHTML;")
99
+ return f"<!DOCTYPE html>\n{html}"
100
+ except Exception as e:
101
+ self.errors.append(f"Erro ao capturar HTML: {str(e)}")
102
+ return self.driver.page_source
103
+
104
+ def capture_all_stylesheets(self):
105
+ """Captura todos os CSS externos e inline."""
106
+ stylesheets = {}
107
+
108
+ try:
109
+ # CSS externos via <link>
110
+ css_links = self.driver.execute_script("""
111
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
112
+ var urls = [];
113
+ links.forEach(function(link) {
114
+ if (link.href) urls.push(link.href);
115
+ });
116
+ return urls;
117
+ """)
118
+
119
+ for css_url in css_links:
120
+ content = self._download_asset(css_url)
121
+ if content:
122
+ filename = self._safe_filename(css_url, ".css")
123
+ stylesheets[f"css/{filename}"] = content
124
+
125
+ # CSS inline via <style>
126
+ inline_styles = self.driver.execute_script("""
127
+ var styles = document.querySelectorAll('style');
128
+ var contents = [];
129
+ styles.forEach(function(s, i) {
130
+ contents.push(s.textContent || s.innerText || '');
131
+ });
132
+ return contents;
133
+ """)
134
+
135
+ for i, style_content in enumerate(inline_styles):
136
+ if style_content.strip():
137
+ stylesheets[f"css/inline_style_{i+1}.css"] = style_content.encode('utf-8')
138
+
139
+ except Exception as e:
140
+ self.errors.append(f"Erro ao capturar CSS: {str(e)}")
141
+
142
+ return stylesheets
143
+
144
+ def capture_all_scripts(self):
145
+ """Captura todos os JS externos e inline."""
146
+ scripts = {}
147
+
148
+ try:
149
+ # JS externos
150
+ js_urls = self.driver.execute_script("""
151
+ var scripts = document.querySelectorAll('script[src]');
152
+ var urls = [];
153
+ scripts.forEach(function(s) {
154
+ if (s.src) urls.push(s.src);
155
+ });
156
+ return urls;
157
+ """)
158
+
159
+ for js_url in js_urls:
160
+ content = self._download_asset(js_url)
161
+ if content:
162
+ filename = self._safe_filename(js_url, ".js")
163
+ scripts[f"js/{filename}"] = content
164
+
165
+ # JS inline
166
+ inline_scripts = self.driver.execute_script("""
167
+ var scripts = document.querySelectorAll('script:not([src])');
168
+ var contents = [];
169
+ scripts.forEach(function(s, i) {
170
+ var text = s.textContent || s.innerText || '';
171
+ if (text.trim().length > 0) contents.push(text);
172
+ });
173
+ return contents;
174
+ """)
175
+
176
+ for i, script_content in enumerate(inline_scripts):
177
+ if script_content.strip():
178
+ scripts[f"js/inline_script_{i+1}.js"] = script_content.encode('utf-8')
179
+
180
+ except Exception as e:
181
+ self.errors.append(f"Erro ao capturar JS: {str(e)}")
182
+
183
+ return scripts
184
+
185
+ def capture_all_images(self):
186
+ """Captura todas as imagens do site."""
187
+ images = {}
188
+
189
+ try:
190
+ img_urls = self.driver.execute_script("""
191
+ var images = document.querySelectorAll('img');
192
+ var urls = [];
193
+ images.forEach(function(img) {
194
+ if (img.src && !img.src.startsWith('data:')) urls.push(img.src);
195
+ if (img.dataset && img.dataset.src) urls.push(img.dataset.src);
196
+ });
197
+ // Background images via CSS
198
+ var allElements = document.querySelectorAll('*');
199
+ allElements.forEach(function(el) {
200
+ var bg = window.getComputedStyle(el).backgroundImage;
201
+ if (bg && bg !== 'none') {
202
+ var match = bg.match(/url\\(['"]?(.+?)['"]?\\)/);
203
+ if (match && !match[1].startsWith('data:')) {
204
+ urls.push(match[1]);
205
+ }
206
+ }
207
+ });
208
+ return [...new Set(urls)];
209
+ """)
210
+
211
+ for img_url in img_urls:
212
+ absolute_url = urljoin(self.url, img_url)
213
+ content = self._download_asset(absolute_url)
214
+ if content:
215
+ filename = self._safe_filename(absolute_url)
216
+ images[f"images/{filename}"] = content
217
+
218
+ except Exception as e:
219
+ self.errors.append(f"Erro ao capturar imagens: {str(e)}")
220
+
221
+ return images
222
+
223
+ def capture_fonts(self):
224
+ """Captura todas as fontes usadas no site."""
225
+ fonts = {}
226
+
227
+ try:
228
+ font_urls = self.driver.execute_script("""
229
+ var urls = [];
230
+ for (var i = 0; i < document.styleSheets.length; i++) {
231
+ try {
232
+ var rules = document.styleSheets[i].cssRules ||
233
+ document.styleSheets[i].rules;
234
+ if (!rules) continue;
235
+ for (var j = 0; j < rules.length; j++) {
236
+ if (rules[j].type === CSSRule.FONT_FACE_RULE) {
237
+ var src = rules[j].style.getPropertyValue('src');
238
+ var matches = src.match(/url\\(['"]?(.+?)['"]?\\)/g);
239
+ if (matches) {
240
+ matches.forEach(function(m) {
241
+ var url = m.replace(/url\\(['"]?/, '')
242
+ .replace(/['"]?\\)/, '');
243
+ if (!url.startsWith('data:')) urls.push(url);
244
+ });
245
+ }
246
+ }
247
+ }
248
+ } catch(e) {}
249
+ }
250
+ return [...new Set(urls)];
251
+ """)
252
+
253
+ for font_url in font_urls:
254
+ absolute_url = urljoin(self.url, font_url)
255
+ content = self._download_asset(absolute_url)
256
+ if content:
257
+ filename = self._safe_filename(absolute_url)
258
+ fonts[f"fonts/{filename}"] = content
259
+
260
+ except Exception as e:
261
+ self.errors.append(f"Erro ao capturar fontes: {str(e)}")
262
+
263
+ return fonts
264
+
265
+ def capture_screenshot(self):
266
+ """Captura um screenshot da pΓ‘gina."""
267
+ try:
268
+ return self.driver.get_screenshot_as_png()
269
+ except Exception as e:
270
+ self.errors.append(f"Erro ao capturar screenshot: {str(e)}")
271
+ return None
272
+
273
+ def generate_backup_zip(self):
274
+ """Gera o ZIP completo do backup."""
275
+ zip_buffer = BytesIO()
276
+
277
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
278
+ # 1. HTML principal
279
+ html = self.capture_full_html()
280
+ zf.writestr("index.html", html)
281
+
282
+ # 2. CSS
283
+ stylesheets = self.capture_all_stylesheets()
284
+ for path, content in stylesheets.items():
285
+ zf.writestr(path, content)
286
+
287
+ # 3. JavaScript
288
+ scripts = self.capture_all_scripts()
289
+ for path, content in scripts.items():
290
+ zf.writestr(path, content)
291
+
292
+ # 4. Imagens
293
+ images = self.capture_all_images()
294
+ for path, content in images.items():
295
+ zf.writestr(path, content)
296
+
297
+ # 5. Fontes
298
+ fonts = self.capture_fonts()
299
+ for path, content in fonts.items():
300
+ zf.writestr(path, content)
301
+
302
+ # 6. Screenshot
303
+ screenshot = self.capture_screenshot()
304
+ if screenshot:
305
+ zf.writestr("screenshot.png", screenshot)
306
+
307
+ # 7. RelatΓ³rio do backup
308
+ report = self._generate_report(
309
+ html, stylesheets, scripts, images, fonts
310
+ )
311
+ zf.writestr("backup_report.txt", report)
312
+
313
+ # 8. Erros durante o backup
314
+ if self.errors:
315
+ error_log = "ERROS DURANTE O BACKUP\n"
316
+ error_log += "=" * 50 + "\n\n"
317
+ for err in self.errors:
318
+ error_log += f"- {err}\n"
319
+ zf.writestr("backup_errors.txt", error_log)
320
+
321
+ zip_buffer.seek(0)
322
+ return zip_buffer
323
+
324
+ def _generate_report(self, html, stylesheets, scripts, images, fonts):
325
+ """Gera relatΓ³rio textual do backup."""
326
+ report = []
327
+ report.append("=" * 60)
328
+ report.append(" RELATΓ“RIO DE BACKUP DO SITE")
329
+ report.append("=" * 60)
330
+ report.append(f"\nURL Original: {self.url}")
331
+ report.append(f"DomΓ­nio: {self.domain}")
332
+ report.append(f"Data do Backup: {time.strftime('%d/%m/%Y %H:%M:%S')}")
333
+ report.append(f"\n{'─' * 40}")
334
+ report.append("ARQUIVOS CAPTURADOS:")
335
+ report.append(f"{'─' * 40}")
336
+ report.append(f" HTML: 1 arquivo")
337
+ report.append(f" CSS: {len(stylesheets)} arquivo(s)")
338
+ report.append(f" JS: {len(scripts)} arquivo(s)")
339
+ report.append(f" Imagens: {len(images)} arquivo(s)")
340
+ report.append(f" Fontes: {len(fonts)} arquivo(s)")
341
+ report.append(f" Screenshot: 1 arquivo")
342
+ report.append(f"\n TOTAL: {1 + len(stylesheets) + len(scripts) + len(images) + len(fonts) + 1} arquivos")
343
+ report.append(f"\n{'─' * 40}")
344
+
345
+ if self.errors:
346
+ report.append(f"\nAVISOS ({len(self.errors)}):")
347
+ for err in self.errors:
348
+ report.append(f" ⚠ {err}")
349
+
350
+ report.append(f"\n{'=' * 60}")
351
+ report.append("Backup gerado pelo Site Backup System")
352
+ report.append("=" * 60)
353
+
354
+ return "\n".join(report)
utils/error_checker.py ADDED
@@ -0,0 +1,1106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import json
3
+ import re
4
+ import requests
5
+ from urllib.parse import urljoin, urlparse
6
+ from selenium.webdriver.common.by import By
7
+ from selenium.webdriver.support.ui import WebDriverWait
8
+ from selenium.webdriver.support import expected_conditions as EC
9
+
10
+
11
+ class SiteErrorChecker:
12
+ """Classe responsΓ‘vel por encontrar todos os erros de um site."""
13
+
14
+ def __init__(self, driver, url):
15
+ self.driver = driver
16
+ self.url = url
17
+ self.domain = urlparse(url).netloc
18
+ self.errors = {
19
+ "console_errors": [],
20
+ "console_warnings": [],
21
+ "javascript_errors": [],
22
+ "network_errors": [],
23
+ "resource_errors": [],
24
+ "css_errors": [],
25
+ "html_errors": [],
26
+ "accessibility_errors": [],
27
+ "security_warnings": [],
28
+ "performance_warnings": [],
29
+ "broken_links": [],
30
+ "seo_warnings": [],
31
+ }
32
+ self.total_errors = 0
33
+ self.total_warnings = 0
34
+
35
+ def run_all_checks(self):
36
+ """Executa TODAS as verificaΓ§Γ΅es de erro."""
37
+ self.check_console_logs()
38
+ self.check_javascript_errors()
39
+ self.check_network_errors()
40
+ self.check_resource_loading()
41
+ self.check_css_errors()
42
+ self.check_html_errors()
43
+ self.check_accessibility()
44
+ self.check_security()
45
+ self.check_performance()
46
+ self.check_broken_links()
47
+ self.check_seo()
48
+
49
+ self._count_totals()
50
+ return self.errors
51
+
52
+ # ─────────────────────────────────────────────
53
+ # 1. ERROS DO CONSOLE DO NAVEGADOR
54
+ # ─────────────────────────────────────────────
55
+ def check_console_logs(self):
56
+ """Captura todos os logs do console do navegador."""
57
+ try:
58
+ logs = self.driver.get_log("browser")
59
+ for log_entry in logs:
60
+ level = log_entry.get("level", "").upper()
61
+ message = log_entry.get("message", "")
62
+ source = log_entry.get("source", "unknown")
63
+ timestamp = log_entry.get("timestamp", 0)
64
+
65
+ entry = {
66
+ "level": level,
67
+ "message": message,
68
+ "source": source,
69
+ "timestamp": time.strftime(
70
+ '%H:%M:%S',
71
+ time.localtime(timestamp / 1000)
72
+ ) if timestamp else "N/A"
73
+ }
74
+
75
+ if level == "SEVERE":
76
+ self.errors["console_errors"].append(entry)
77
+ elif level == "WARNING":
78
+ self.errors["console_warnings"].append(entry)
79
+
80
+ except Exception as e:
81
+ self.errors["console_errors"].append({
82
+ "level": "INFO",
83
+ "message": f"NΓ£o foi possΓ­vel acessar logs do console: {str(e)}",
84
+ "source": "checker",
85
+ "timestamp": "N/A"
86
+ })
87
+
88
+ # ─────────────────────────────────────────────
89
+ # 2. ERROS DE JAVASCRIPT
90
+ # ─────────────────────────────────────────────
91
+ def check_javascript_errors(self):
92
+ """Verifica erros de JavaScript na pΓ‘gina."""
93
+ try:
94
+ js_errors = self.driver.execute_script("""
95
+ var errors = [];
96
+
97
+ // Verificar se hΓ‘ variΓ‘veis nΓ£o definidas comuns
98
+ var commonChecks = [
99
+ 'jQuery', '$', 'React', 'ReactDOM', 'Vue',
100
+ 'angular', 'ng', 'bootstrap'
101
+ ];
102
+
103
+ // Verificar scripts com erro de carregamento
104
+ var scripts = document.querySelectorAll('script[src]');
105
+ scripts.forEach(function(script) {
106
+ if (script.src) {
107
+ try {
108
+ var xhr = new XMLHttpRequest();
109
+ xhr.open('HEAD', script.src, false);
110
+ xhr.send();
111
+ if (xhr.status >= 400) {
112
+ errors.push({
113
+ type: 'SCRIPT_LOAD_ERROR',
114
+ message: 'Falha ao carregar script: ' + script.src,
115
+ status: xhr.status,
116
+ url: script.src
117
+ });
118
+ }
119
+ } catch(e) {
120
+ errors.push({
121
+ type: 'SCRIPT_BLOCKED',
122
+ message: 'Script bloqueado ou inacessΓ­vel: ' + script.src,
123
+ status: 0,
124
+ url: script.src
125
+ });
126
+ }
127
+ }
128
+ });
129
+
130
+ // Verificar erros de execuΓ§Γ£o inline
131
+ var inlineScripts = document.querySelectorAll('script:not([src])');
132
+ var inlineCount = 0;
133
+ inlineScripts.forEach(function(script) {
134
+ inlineCount++;
135
+ var content = script.textContent || '';
136
+ // Verificar padrΓ΅es suspeitos
137
+ if (content.includes('undefined') && content.includes('null')) {
138
+ errors.push({
139
+ type: 'POTENTIAL_ISSUE',
140
+ message: 'Script inline #' + inlineCount +
141
+ ' pode ter referΓͺncias nulas',
142
+ status: 0,
143
+ url: 'inline'
144
+ });
145
+ }
146
+ });
147
+
148
+ // Verificar event listeners com erro
149
+ try {
150
+ var allElements = document.querySelectorAll('[onclick], [onerror], [onload]');
151
+ allElements.forEach(function(el) {
152
+ var onclick = el.getAttribute('onclick');
153
+ if (onclick) {
154
+ try {
155
+ new Function(onclick);
156
+ } catch(e) {
157
+ errors.push({
158
+ type: 'INLINE_HANDLER_ERROR',
159
+ message: 'Erro em handler inline: ' + e.message,
160
+ status: 0,
161
+ url: el.tagName + ' element'
162
+ });
163
+ }
164
+ }
165
+ });
166
+ } catch(e) {}
167
+
168
+ return errors;
169
+ """)
170
+
171
+ for err in js_errors:
172
+ self.errors["javascript_errors"].append(err)
173
+
174
+ except Exception as e:
175
+ self.errors["javascript_errors"].append({
176
+ "type": "CHECK_FAILED",
177
+ "message": f"Erro ao verificar JS: {str(e)}",
178
+ "status": 0,
179
+ "url": self.url
180
+ })
181
+
182
+ # ─────────────────────────────────────────────
183
+ # 3. ERROS DE REDE
184
+ # ─────────────────────────────────────────────
185
+ def check_network_errors(self):
186
+ """Verifica erros de rede e recursos que falharam."""
187
+ try:
188
+ network_data = self.driver.execute_script("""
189
+ var entries = performance.getEntriesByType('resource');
190
+ var results = [];
191
+ entries.forEach(function(entry) {
192
+ var data = {
193
+ name: entry.name,
194
+ type: entry.initiatorType,
195
+ duration: Math.round(entry.duration),
196
+ size: entry.transferSize || 0,
197
+ status: 'ok'
198
+ };
199
+ // Se a duraΓ§Γ£o Γ© 0 e o tamanho Γ© 0, pode ter falhado
200
+ if (entry.transferSize === 0 && entry.duration === 0
201
+ && entry.initiatorType !== 'beacon') {
202
+ data.status = 'possible_failure';
203
+ }
204
+ // Se demorou muito (> 5 segundos)
205
+ if (entry.duration > 5000) {
206
+ data.status = 'slow';
207
+ }
208
+ results.push(data);
209
+ });
210
+ return results;
211
+ """)
212
+
213
+ for entry in network_data:
214
+ if entry["status"] == "possible_failure":
215
+ self.errors["network_errors"].append({
216
+ "type": "RESOURCE_FAILURE",
217
+ "message": f"Recurso pode ter falhado ao carregar: {entry['name']}",
218
+ "resource_type": entry["type"],
219
+ "duration": entry["duration"]
220
+ })
221
+ elif entry["status"] == "slow":
222
+ self.errors["performance_warnings"].append({
223
+ "type": "SLOW_RESOURCE",
224
+ "message": f"Recurso lento ({entry['duration']}ms): {entry['name']}",
225
+ "resource_type": entry["type"],
226
+ "duration": entry["duration"]
227
+ })
228
+
229
+ except Exception as e:
230
+ self.errors["network_errors"].append({
231
+ "type": "CHECK_FAILED",
232
+ "message": f"Erro ao verificar rede: {str(e)}",
233
+ "resource_type": "unknown",
234
+ "duration": 0
235
+ })
236
+
237
+ # ─────────────────────────────────────────────
238
+ # 4. ERROS DE RECURSOS (IMAGENS, SCRIPTS, ETC)
239
+ # ─────────────────────────────────────────────
240
+ def check_resource_loading(self):
241
+ """Verifica se todos os recursos carregaram corretamente."""
242
+ try:
243
+ resource_errors = self.driver.execute_script("""
244
+ var errors = [];
245
+
246
+ // Imagens quebradas
247
+ var images = document.querySelectorAll('img');
248
+ images.forEach(function(img) {
249
+ if (!img.complete || img.naturalWidth === 0) {
250
+ if (img.src && !img.src.startsWith('data:')) {
251
+ errors.push({
252
+ type: 'BROKEN_IMAGE',
253
+ tag: 'img',
254
+ url: img.src,
255
+ alt: img.alt || '(sem alt)'
256
+ });
257
+ }
258
+ }
259
+ });
260
+
261
+ // Iframes que falharam
262
+ var iframes = document.querySelectorAll('iframe');
263
+ iframes.forEach(function(iframe) {
264
+ try {
265
+ if (iframe.src && !iframe.contentDocument &&
266
+ !iframe.src.startsWith('about:')) {
267
+ errors.push({
268
+ type: 'IFRAME_ERROR',
269
+ tag: 'iframe',
270
+ url: iframe.src,
271
+ alt: ''
272
+ });
273
+ }
274
+ } catch(e) {
275
+ // Cross-origin, normal
276
+ }
277
+ });
278
+
279
+ // VΓ­deos e Γ‘udios com erro
280
+ var media = document.querySelectorAll('video, audio');
281
+ media.forEach(function(m) {
282
+ if (m.error) {
283
+ errors.push({
284
+ type: 'MEDIA_ERROR',
285
+ tag: m.tagName.toLowerCase(),
286
+ url: m.src || m.querySelector('source')?.src || 'unknown',
287
+ alt: 'Error code: ' + m.error.code
288
+ });
289
+ }
290
+ });
291
+
292
+ // Fontes que falharam
293
+ if (document.fonts) {
294
+ document.fonts.forEach(function(font) {
295
+ if (font.status === 'error') {
296
+ errors.push({
297
+ type: 'FONT_ERROR',
298
+ tag: 'font',
299
+ url: font.family,
300
+ alt: font.style + ' ' + font.weight
301
+ });
302
+ }
303
+ });
304
+ }
305
+
306
+ return errors;
307
+ """)
308
+
309
+ for err in resource_errors:
310
+ self.errors["resource_errors"].append(err)
311
+
312
+ except Exception as e:
313
+ self.errors["resource_errors"].append({
314
+ "type": "CHECK_FAILED",
315
+ "tag": "unknown",
316
+ "url": str(e),
317
+ "alt": ""
318
+ })
319
+
320
+ # ─────────────────────────────────────────────
321
+ # 5. ERROS DE CSS
322
+ # ─────────────────────────────────────────────
323
+ def check_css_errors(self):
324
+ """Verifica erros de CSS."""
325
+ try:
326
+ css_errors = self.driver.execute_script("""
327
+ var errors = [];
328
+
329
+ // Verificar stylesheets que falharam ao carregar
330
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
331
+ links.forEach(function(link) {
332
+ try {
333
+ var sheet = link.sheet;
334
+ if (!sheet) {
335
+ errors.push({
336
+ type: 'CSS_LOAD_ERROR',
337
+ message: 'Stylesheet nΓ£o carregou: ' + link.href,
338
+ url: link.href
339
+ });
340
+ } else {
341
+ try {
342
+ var rules = sheet.cssRules;
343
+ } catch(e) {
344
+ if (e.name === 'SecurityError') {
345
+ // Cross-origin, nΓ£o Γ© erro real
346
+ } else {
347
+ errors.push({
348
+ type: 'CSS_ACCESS_ERROR',
349
+ message: 'Erro ao acessar regras CSS: ' +
350
+ e.message,
351
+ url: link.href
352
+ });
353
+ }
354
+ }
355
+ }
356
+ } catch(e) {
357
+ errors.push({
358
+ type: 'CSS_PARSE_ERROR',
359
+ message: 'Erro ao processar stylesheet: ' + e.message,
360
+ url: link.href || 'unknown'
361
+ });
362
+ }
363
+ });
364
+
365
+ // Verificar elementos com estilos invΓ‘lidos
366
+ var allElements = document.querySelectorAll('*');
367
+ var checkedProps = ['display', 'position', 'z-index', 'overflow'];
368
+ var elementCount = 0;
369
+ allElements.forEach(function(el) {
370
+ if (elementCount > 500) return; // Limitar para performance
371
+ elementCount++;
372
+ var style = window.getComputedStyle(el);
373
+
374
+ // Verificar z-index absurdamente alto
375
+ var zIndex = parseInt(style.zIndex);
376
+ if (zIndex > 999999) {
377
+ errors.push({
378
+ type: 'CSS_WARNING',
379
+ message: 'z-index muito alto (' + zIndex +
380
+ ') em ' + el.tagName +
381
+ (el.id ? '#' + el.id : '') +
382
+ (el.className ? '.' +
383
+ String(el.className).split(' ')[0] : ''),
384
+ url: 'inline'
385
+ });
386
+ }
387
+ });
388
+
389
+ return errors;
390
+ """)
391
+
392
+ for err in css_errors:
393
+ self.errors["css_errors"].append(err)
394
+
395
+ except Exception as e:
396
+ self.errors["css_errors"].append({
397
+ "type": "CHECK_FAILED",
398
+ "message": f"Erro ao verificar CSS: {str(e)}",
399
+ "url": self.url
400
+ })
401
+
402
+ # ─────────────────────────────────────────────
403
+ # 6. ERROS DE HTML
404
+ # ─────────────────────────────────────────────
405
+ def check_html_errors(self):
406
+ """Verifica erros e problemas na estrutura HTML."""
407
+ try:
408
+ html_issues = self.driver.execute_script("""
409
+ var issues = [];
410
+
411
+ // Verificar DOCTYPE
412
+ if (!document.doctype) {
413
+ issues.push({
414
+ type: 'MISSING_DOCTYPE',
415
+ message: 'PΓ‘gina sem DOCTYPE declarado',
416
+ element: 'document'
417
+ });
418
+ }
419
+
420
+ // Verificar <html lang="">
421
+ var htmlEl = document.documentElement;
422
+ if (!htmlEl.lang) {
423
+ issues.push({
424
+ type: 'MISSING_LANG',
425
+ message: 'Atributo lang nΓ£o definido no <html>',
426
+ element: 'html'
427
+ });
428
+ }
429
+
430
+ // Verificar <title>
431
+ if (!document.title || document.title.trim() === '') {
432
+ issues.push({
433
+ type: 'MISSING_TITLE',
434
+ message: 'PΓ‘gina sem tΓ­tulo (<title> vazio ou ausente)',
435
+ element: 'head'
436
+ });
437
+ }
438
+
439
+ // Verificar meta charset
440
+ var charset = document.querySelector('meta[charset]');
441
+ if (!charset) {
442
+ issues.push({
443
+ type: 'MISSING_CHARSET',
444
+ message: 'Meta charset nΓ£o declarado',
445
+ element: 'head'
446
+ });
447
+ }
448
+
449
+ // Verificar meta viewport
450
+ var viewport = document.querySelector('meta[name="viewport"]');
451
+ if (!viewport) {
452
+ issues.push({
453
+ type: 'MISSING_VIEWPORT',
454
+ message: 'Meta viewport nΓ£o declarado (problemas em mobile)',
455
+ element: 'head'
456
+ });
457
+ }
458
+
459
+ // Imagens sem alt
460
+ var imgs = document.querySelectorAll('img');
461
+ var noAltCount = 0;
462
+ imgs.forEach(function(img) {
463
+ if (!img.hasAttribute('alt')) noAltCount++;
464
+ });
465
+ if (noAltCount > 0) {
466
+ issues.push({
467
+ type: 'MISSING_ALT',
468
+ message: noAltCount + ' imagem(ns) sem atributo alt',
469
+ element: 'img'
470
+ });
471
+ }
472
+
473
+ // Verificar IDs duplicados
474
+ var allIds = document.querySelectorAll('[id]');
475
+ var idMap = {};
476
+ allIds.forEach(function(el) {
477
+ var id = el.id;
478
+ if (idMap[id]) {
479
+ issues.push({
480
+ type: 'DUPLICATE_ID',
481
+ message: 'ID duplicado encontrado: #' + id,
482
+ element: el.tagName
483
+ });
484
+ }
485
+ idMap[id] = true;
486
+ });
487
+
488
+ // Verificar links vazios
489
+ var links = document.querySelectorAll('a');
490
+ links.forEach(function(a) {
491
+ var href = a.getAttribute('href');
492
+ if (!href || href === '#' || href === 'javascript:void(0)') {
493
+ var text = (a.textContent || '').trim().substring(0, 50);
494
+ if (text) {
495
+ issues.push({
496
+ type: 'EMPTY_LINK',
497
+ message: 'Link sem destino vΓ‘lido: "' + text + '"',
498
+ element: 'a'
499
+ });
500
+ }
501
+ }
502
+ });
503
+
504
+ // Verificar formulΓ‘rios sem action
505
+ var forms = document.querySelectorAll('form');
506
+ forms.forEach(function(form, i) {
507
+ if (!form.action || form.action === window.location.href) {
508
+ issues.push({
509
+ type: 'FORM_NO_ACTION',
510
+ message: 'FormulΓ‘rio #' + (i+1) +
511
+ ' sem action definido',
512
+ element: 'form'
513
+ });
514
+ }
515
+ });
516
+
517
+ // Verificar hierarquia de headings
518
+ var headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
519
+ var h1Count = document.querySelectorAll('h1').length;
520
+ if (h1Count === 0) {
521
+ issues.push({
522
+ type: 'NO_H1',
523
+ message: 'PΓ‘gina nΓ£o tem nenhum <h1>',
524
+ element: 'headings'
525
+ });
526
+ } else if (h1Count > 1) {
527
+ issues.push({
528
+ type: 'MULTIPLE_H1',
529
+ message: 'PΓ‘gina tem ' + h1Count + ' tags <h1> (recomendado: 1)',
530
+ element: 'headings'
531
+ });
532
+ }
533
+
534
+ return issues;
535
+ """)
536
+
537
+ for issue in html_issues:
538
+ self.errors["html_errors"].append(issue)
539
+
540
+ except Exception as e:
541
+ self.errors["html_errors"].append({
542
+ "type": "CHECK_FAILED",
543
+ "message": f"Erro ao verificar HTML: {str(e)}",
544
+ "element": "unknown"
545
+ })
546
+
547
+ # ─────────────────────────────────────────────
548
+ # 7. ACESSIBILIDADE
549
+ # ─────────────────────────────────────────────
550
+ def check_accessibility(self):
551
+ """Verifica problemas de acessibilidade."""
552
+ try:
553
+ a11y_issues = self.driver.execute_script("""
554
+ var issues = [];
555
+
556
+ // Inputs sem label
557
+ var inputs = document.querySelectorAll(
558
+ 'input:not([type="hidden"]):not([type="submit"])' +
559
+ ':not([type="button"]), select, textarea'
560
+ );
561
+ inputs.forEach(function(input) {
562
+ var id = input.id;
563
+ var ariaLabel = input.getAttribute('aria-label');
564
+ var ariaLabelledBy = input.getAttribute('aria-labelledby');
565
+ var label = id ?
566
+ document.querySelector('label[for="' + id + '"]') :
567
+ null;
568
+ var parentLabel = input.closest('label');
569
+
570
+ if (!label && !parentLabel && !ariaLabel && !ariaLabelledBy) {
571
+ issues.push({
572
+ type: 'INPUT_NO_LABEL',
573
+ message: 'Input sem label acessΓ­vel: ' +
574
+ (input.type || input.tagName) +
575
+ (input.name ? ' [name=' + input.name + ']' : ''),
576
+ severity: 'error'
577
+ });
578
+ }
579
+ });
580
+
581
+ // BotΓ΅es sem texto acessΓ­vel
582
+ var buttons = document.querySelectorAll('button, [role="button"]');
583
+ buttons.forEach(function(btn) {
584
+ var text = (btn.textContent || '').trim();
585
+ var ariaLabel = btn.getAttribute('aria-label');
586
+ var title = btn.getAttribute('title');
587
+ if (!text && !ariaLabel && !title) {
588
+ issues.push({
589
+ type: 'BUTTON_NO_TEXT',
590
+ message: 'BotΓ£o sem texto acessΓ­vel',
591
+ severity: 'error'
592
+ });
593
+ }
594
+ });
595
+
596
+ // Contraste (verificaΓ§Γ£o bΓ‘sica de texto branco em fundo claro)
597
+ var textElements = document.querySelectorAll('p, span, a, li, h1, h2, h3, h4, h5, h6');
598
+ var contrastCount = 0;
599
+ textElements.forEach(function(el) {
600
+ if (contrastCount > 100) return;
601
+ contrastCount++;
602
+ var style = window.getComputedStyle(el);
603
+ var color = style.color;
604
+ var fontSize = parseFloat(style.fontSize);
605
+ if (fontSize < 12) {
606
+ issues.push({
607
+ type: 'SMALL_TEXT',
608
+ message: 'Texto muito pequeno (' + fontSize +
609
+ 'px): "' +
610
+ (el.textContent || '').trim().substring(0, 30) +
611
+ '"',
612
+ severity: 'warning'
613
+ });
614
+ }
615
+ });
616
+
617
+ // Verificar tabindex negativo
618
+ var negTabindex = document.querySelectorAll('[tabindex]');
619
+ negTabindex.forEach(function(el) {
620
+ var val = parseInt(el.getAttribute('tabindex'));
621
+ if (val > 0) {
622
+ issues.push({
623
+ type: 'POSITIVE_TABINDEX',
624
+ message: 'tabindex positivo (' + val +
625
+ ') pode causar navegaΓ§Γ£o confusa em: ' +
626
+ el.tagName,
627
+ severity: 'warning'
628
+ });
629
+ }
630
+ });
631
+
632
+ // Verificar ARIA roles invΓ‘lidos
633
+ var ariaElements = document.querySelectorAll('[role]');
634
+ var validRoles = [
635
+ 'alert','alertdialog','application','article','banner',
636
+ 'button','cell','checkbox','columnheader','combobox',
637
+ 'complementary','contentinfo','definition','dialog',
638
+ 'directory','document','feed','figure','form','grid',
639
+ 'gridcell','group','heading','img','link','list',
640
+ 'listbox','listitem','log','main','marquee','math',
641
+ 'menu','menubar','menuitem','menuitemcheckbox',
642
+ 'menuitemradio','navigation','none','note','option',
643
+ 'presentation','progressbar','radio','radiogroup',
644
+ 'region','row','rowgroup','rowheader','scrollbar',
645
+ 'search','searchbox','separator','slider','spinbutton',
646
+ 'status','switch','tab','table','tablist','tabpanel',
647
+ 'term','textbox','timer','toolbar','tooltip','tree',
648
+ 'treegrid','treeitem'
649
+ ];
650
+ ariaElements.forEach(function(el) {
651
+ var role = el.getAttribute('role');
652
+ if (validRoles.indexOf(role) === -1) {
653
+ issues.push({
654
+ type: 'INVALID_ROLE',
655
+ message: 'Role ARIA invΓ‘lido: "' + role + '"',
656
+ severity: 'error'
657
+ });
658
+ }
659
+ });
660
+
661
+ return issues;
662
+ """)
663
+
664
+ for issue in a11y_issues:
665
+ self.errors["accessibility_errors"].append(issue)
666
+
667
+ except Exception as e:
668
+ self.errors["accessibility_errors"].append({
669
+ "type": "CHECK_FAILED",
670
+ "message": f"Erro ao verificar acessibilidade: {str(e)}",
671
+ "severity": "info"
672
+ })
673
+
674
+ # ─────────────────────────────────────────────
675
+ # 8. SEGURANÇA
676
+ # ─────────────────────────────────────��───────
677
+ def check_security(self):
678
+ """Verifica problemas de seguranΓ§a bΓ‘sicos."""
679
+ try:
680
+ security_issues = self.driver.execute_script("""
681
+ var issues = [];
682
+
683
+ // Verificar HTTPS
684
+ if (window.location.protocol !== 'https:') {
685
+ issues.push({
686
+ type: 'NO_HTTPS',
687
+ message: 'Site nΓ£o usa HTTPS'
688
+ });
689
+ }
690
+
691
+ // Verificar mixed content (recursos HTTP em pΓ‘gina HTTPS)
692
+ if (window.location.protocol === 'https:') {
693
+ var allResources = document.querySelectorAll(
694
+ 'img[src^="http:"], script[src^="http:"], ' +
695
+ 'link[href^="http:"], iframe[src^="http:"]'
696
+ );
697
+ allResources.forEach(function(el) {
698
+ var url = el.src || el.href;
699
+ issues.push({
700
+ type: 'MIXED_CONTENT',
701
+ message: 'ConteΓΊdo misto (HTTP em HTTPS): ' + url
702
+ });
703
+ });
704
+ }
705
+
706
+ // Verificar links target="_blank" sem rel="noopener"
707
+ var blankLinks = document.querySelectorAll('a[target="_blank"]');
708
+ blankLinks.forEach(function(a) {
709
+ var rel = (a.getAttribute('rel') || '').toLowerCase();
710
+ if (!rel.includes('noopener') && !rel.includes('noreferrer')) {
711
+ issues.push({
712
+ type: 'UNSAFE_BLANK_LINK',
713
+ message: 'Link com target="_blank" sem rel="noopener": ' +
714
+ (a.href || '').substring(0, 80)
715
+ });
716
+ }
717
+ });
718
+
719
+ // Verificar forms inseguros
720
+ var forms = document.querySelectorAll('form');
721
+ forms.forEach(function(form) {
722
+ var action = form.action || '';
723
+ if (action.startsWith('http:') &&
724
+ window.location.protocol === 'https:') {
725
+ issues.push({
726
+ type: 'INSECURE_FORM',
727
+ message: 'FormulΓ‘rio envia dados via HTTP inseguro'
728
+ });
729
+ }
730
+
731
+ // Verificar autocomplete em campos sensΓ­veis
732
+ var passwordFields = form.querySelectorAll(
733
+ 'input[type="password"]'
734
+ );
735
+ passwordFields.forEach(function(pw) {
736
+ if (pw.getAttribute('autocomplete') === 'on') {
737
+ issues.push({
738
+ type: 'PASSWORD_AUTOCOMPLETE',
739
+ message: 'Campo de senha com autocomplete habilitado'
740
+ });
741
+ }
742
+ });
743
+ });
744
+
745
+ // Verificar inline event handlers (potencial XSS)
746
+ var inlineHandlers = document.querySelectorAll(
747
+ '[onload], [onerror], [onclick], [onmouseover]'
748
+ );
749
+ if (inlineHandlers.length > 10) {
750
+ issues.push({
751
+ type: 'EXCESSIVE_INLINE_HANDLERS',
752
+ message: inlineHandlers.length +
753
+ ' elementos com event handlers inline ' +
754
+ '(considere usar addEventListener)'
755
+ });
756
+ }
757
+
758
+ return issues;
759
+ """)
760
+
761
+ for issue in security_issues:
762
+ self.errors["security_warnings"].append(issue)
763
+
764
+ except Exception as e:
765
+ self.errors["security_warnings"].append({
766
+ "type": "CHECK_FAILED",
767
+ "message": f"Erro ao verificar seguranΓ§a: {str(e)}"
768
+ })
769
+
770
+ # ─────────────────────────────────────────────
771
+ # 9. PERFORMANCE
772
+ # ─────────────────────────────────────────────
773
+ def check_performance(self):
774
+ """Verifica problemas de performance."""
775
+ try:
776
+ perf_data = self.driver.execute_script("""
777
+ var issues = [];
778
+ var timing = performance.timing;
779
+
780
+ // Tempo de carregamento total
781
+ var loadTime = timing.loadEventEnd - timing.navigationStart;
782
+ if (loadTime > 5000) {
783
+ issues.push({
784
+ type: 'SLOW_LOAD',
785
+ message: 'PΓ‘gina demorou ' +
786
+ (loadTime / 1000).toFixed(2) +
787
+ 's para carregar (recomendado: < 3s)',
788
+ value: loadTime
789
+ });
790
+ }
791
+
792
+ // DOM muito grande
793
+ var domSize = document.querySelectorAll('*').length;
794
+ if (domSize > 1500) {
795
+ issues.push({
796
+ type: 'LARGE_DOM',
797
+ message: 'DOM muito grande: ' + domSize +
798
+ ' elementos (recomendado: < 1500)',
799
+ value: domSize
800
+ });
801
+ }
802
+
803
+ // Muitas requisiΓ§Γ΅es
804
+ var resources = performance.getEntriesByType('resource');
805
+ if (resources.length > 80) {
806
+ issues.push({
807
+ type: 'TOO_MANY_REQUESTS',
808
+ message: resources.length +
809
+ ' requisiΓ§Γ΅es de recursos (recomendado: < 80)',
810
+ value: resources.length
811
+ });
812
+ }
813
+
814
+ // Imagens sem dimensΓ΅es (causa layout shift)
815
+ var imgs = document.querySelectorAll('img');
816
+ var noDimCount = 0;
817
+ imgs.forEach(function(img) {
818
+ if (!img.getAttribute('width') && !img.getAttribute('height') &&
819
+ !img.style.width && !img.style.height) {
820
+ noDimCount++;
821
+ }
822
+ });
823
+ if (noDimCount > 0) {
824
+ issues.push({
825
+ type: 'IMG_NO_DIMENSIONS',
826
+ message: noDimCount +
827
+ ' imagem(ns) sem dimensΓ΅es definidas ' +
828
+ '(causa Layout Shift)',
829
+ value: noDimCount
830
+ });
831
+ }
832
+
833
+ // CSS render-blocking
834
+ var cssLinks = document.querySelectorAll(
835
+ 'link[rel="stylesheet"]:not([media="print"])' +
836
+ ':not([media="none"])'
837
+ );
838
+ if (cssLinks.length > 5) {
839
+ issues.push({
840
+ type: 'MANY_CSS_FILES',
841
+ message: cssLinks.length +
842
+ ' arquivos CSS render-blocking ' +
843
+ '(considere concatenar)',
844
+ value: cssLinks.length
845
+ });
846
+ }
847
+
848
+ // JS no <head> sem defer/async
849
+ var blockingJS = document.querySelectorAll(
850
+ 'head script[src]:not([defer]):not([async])'
851
+ );
852
+ if (blockingJS.length > 0) {
853
+ issues.push({
854
+ type: 'BLOCKING_JS',
855
+ message: blockingJS.length +
856
+ ' script(s) bloqueantes no <head> ' +
857
+ '(adicione defer ou async)',
858
+ value: blockingJS.length
859
+ });
860
+ }
861
+
862
+ return issues;
863
+ """)
864
+
865
+ for issue in perf_data:
866
+ self.errors["performance_warnings"].append(issue)
867
+
868
+ except Exception as e:
869
+ self.errors["performance_warnings"].append({
870
+ "type": "CHECK_FAILED",
871
+ "message": f"Erro ao verificar performance: {str(e)}",
872
+ "value": 0
873
+ })
874
+
875
+ # ─────────────────────────────────────────────
876
+ # 10. LINKS QUEBRADOS
877
+ # ─────────────────────────────────────────────
878
+ def check_broken_links(self):
879
+ """Verifica links quebrados na pΓ‘gina."""
880
+ try:
881
+ links = self.driver.execute_script("""
882
+ var anchors = document.querySelectorAll('a[href]');
883
+ var urls = [];
884
+ anchors.forEach(function(a) {
885
+ var href = a.href;
886
+ if (href && !href.startsWith('javascript:') &&
887
+ !href.startsWith('mailto:') &&
888
+ !href.startsWith('tel:') &&
889
+ !href.startsWith('#') &&
890
+ !href.startsWith('data:')) {
891
+ urls.push({
892
+ url: href,
893
+ text: (a.textContent || '').trim().substring(0, 50)
894
+ });
895
+ }
896
+ });
897
+ // Limitar a 50 links para nΓ£o demorar
898
+ return urls.slice(0, 50);
899
+ """)
900
+
901
+ headers = {
902
+ "User-Agent": "Mozilla/5.0 (compatible; SiteChecker/1.0)"
903
+ }
904
+
905
+ for link_data in links:
906
+ try:
907
+ resp = requests.head(
908
+ link_data["url"],
909
+ headers=headers,
910
+ timeout=8,
911
+ allow_redirects=True,
912
+ verify=False
913
+ )
914
+ if resp.status_code >= 400:
915
+ self.errors["broken_links"].append({
916
+ "url": link_data["url"],
917
+ "text": link_data["text"],
918
+ "status_code": resp.status_code
919
+ })
920
+ except requests.exceptions.Timeout:
921
+ self.errors["broken_links"].append({
922
+ "url": link_data["url"],
923
+ "text": link_data["text"],
924
+ "status_code": "TIMEOUT"
925
+ })
926
+ except requests.exceptions.ConnectionError:
927
+ self.errors["broken_links"].append({
928
+ "url": link_data["url"],
929
+ "text": link_data["text"],
930
+ "status_code": "CONNECTION_ERROR"
931
+ })
932
+ except Exception:
933
+ pass
934
+
935
+ except Exception as e:
936
+ self.errors["broken_links"].append({
937
+ "url": self.url,
938
+ "text": f"Erro ao verificar links: {str(e)}",
939
+ "status_code": "CHECK_FAILED"
940
+ })
941
+
942
+ # ─────────────────────────────────────────────
943
+ # 11. SEO BÁSICO
944
+ # ─────────────────────────────────────────────
945
+ def check_seo(self):
946
+ """Verifica problemas de SEO bΓ‘sicos."""
947
+ try:
948
+ seo_issues = self.driver.execute_script("""
949
+ var issues = [];
950
+
951
+ // Meta description
952
+ var metaDesc = document.querySelector('meta[name="description"]');
953
+ if (!metaDesc) {
954
+ issues.push({
955
+ type: 'NO_META_DESCRIPTION',
956
+ message: 'Sem meta description'
957
+ });
958
+ } else {
959
+ var content = metaDesc.getAttribute('content') || '';
960
+ if (content.length < 50) {
961
+ issues.push({
962
+ type: 'SHORT_META_DESCRIPTION',
963
+ message: 'Meta description muito curta (' +
964
+ content.length + ' chars, recomendado: 120-160)'
965
+ });
966
+ } else if (content.length > 160) {
967
+ issues.push({
968
+ type: 'LONG_META_DESCRIPTION',
969
+ message: 'Meta description muito longa (' +
970
+ content.length + ' chars, recomendado: 120-160)'
971
+ });
972
+ }
973
+ }
974
+
975
+ // Canonical
976
+ var canonical = document.querySelector('link[rel="canonical"]');
977
+ if (!canonical) {
978
+ issues.push({
979
+ type: 'NO_CANONICAL',
980
+ message: 'Sem link canonical definido'
981
+ });
982
+ }
983
+
984
+ // Open Graph
985
+ var ogTitle = document.querySelector('meta[property="og:title"]');
986
+ var ogDesc = document.querySelector(
987
+ 'meta[property="og:description"]'
988
+ );
989
+ var ogImage = document.querySelector('meta[property="og:image"]');
990
+ if (!ogTitle || !ogDesc || !ogImage) {
991
+ issues.push({
992
+ type: 'INCOMPLETE_OG',
993
+ message: 'Open Graph incompleto (faltam: ' +
994
+ (!ogTitle ? 'og:title ' : '') +
995
+ (!ogDesc ? 'og:description ' : '') +
996
+ (!ogImage ? 'og:image' : '') + ')'
997
+ });
998
+ }
999
+
1000
+ // Robots meta
1001
+ var robots = document.querySelector('meta[name="robots"]');
1002
+ if (robots) {
1003
+ var content = (robots.getAttribute('content') || '').toLowerCase();
1004
+ if (content.includes('noindex')) {
1005
+ issues.push({
1006
+ type: 'NOINDEX',
1007
+ message: 'PΓ‘gina marcada como noindex (nΓ£o serΓ‘ indexada)'
1008
+ });
1009
+ }
1010
+ }
1011
+
1012
+ return issues;
1013
+ """)
1014
+
1015
+ for issue in seo_issues:
1016
+ self.errors["seo_warnings"].append(issue)
1017
+
1018
+ except Exception as e:
1019
+ self.errors["seo_warnings"].append({
1020
+ "type": "CHECK_FAILED",
1021
+ "message": f"Erro ao verificar SEO: {str(e)}"
1022
+ })
1023
+
1024
+ # ─────────────────────────────────────────────
1025
+ # CONTAGEM E GERAÇÃO DO RELATΓ“RIO
1026
+ # ─────────────────────────────────────────────
1027
+ def _count_totals(self):
1028
+ """Conta totais de erros e warnings."""
1029
+ error_categories = [
1030
+ "console_errors", "javascript_errors", "network_errors",
1031
+ "resource_errors", "css_errors", "html_errors",
1032
+ "accessibility_errors", "broken_links"
1033
+ ]
1034
+ warning_categories = [
1035
+ "console_warnings", "security_warnings",
1036
+ "performance_warnings", "seo_warnings"
1037
+ ]
1038
+
1039
+ self.total_errors = sum(
1040
+ len(self.errors[cat]) for cat in error_categories
1041
+ )
1042
+ self.total_warnings = sum(
1043
+ len(self.errors[cat]) for cat in warning_categories
1044
+ )
1045
+
1046
+ def generate_report_txt(self):
1047
+ """Gera relatΓ³rio completo em TXT."""
1048
+ lines = []
1049
+ lines.append("=" * 70)
1050
+ lines.append(" RELATΓ“RIO COMPLETO DE ERROS DO SITE")
1051
+ lines.append("=" * 70)
1052
+ lines.append(f"\n URL Analisada: {self.url}")
1053
+ lines.append(f" DomΓ­nio: {self.domain}")
1054
+ lines.append(f" Data da AnΓ‘lise: {time.strftime('%d/%m/%Y %H:%M:%S')}")
1055
+ lines.append(f"\n TOTAL DE ERROS: {self.total_errors}")
1056
+ lines.append(f" TOTAL DE AVISOS: {self.total_warnings}")
1057
+ lines.append(f"\n{'=' * 70}")
1058
+
1059
+ sections = [
1060
+ ("ERROS DO CONSOLE", "console_errors"),
1061
+ ("AVISOS DO CONSOLE", "console_warnings"),
1062
+ ("ERROS DE JAVASCRIPT", "javascript_errors"),
1063
+ ("ERROS DE REDE", "network_errors"),
1064
+ ("ERROS DE RECURSOS", "resource_errors"),
1065
+ ("ERROS DE CSS", "css_errors"),
1066
+ ("ERROS DE HTML", "html_errors"),
1067
+ ("ERROS DE ACESSIBILIDADE", "accessibility_errors"),
1068
+ ("AVISOS DE SEGURANÇA", "security_warnings"),
1069
+ ("AVISOS DE PERFORMANCE", "performance_warnings"),
1070
+ ("LINKS QUEBRADOS", "broken_links"),
1071
+ ("AVISOS DE SEO", "seo_warnings"),
1072
+ ]
1073
+
1074
+ for title, key in sections:
1075
+ items = self.errors[key]
1076
+ lines.append(f"\n{'─' * 70}")
1077
+ lines.append(f" {title} ({len(items)} encontrado(s))")
1078
+ lines.append(f"{'─' * 70}")
1079
+
1080
+ if not items:
1081
+ lines.append(" βœ… Nenhum problema encontrado.")
1082
+ else:
1083
+ for i, item in enumerate(items, 1):
1084
+ lines.append(f"\n [{i}]")
1085
+ if isinstance(item, dict):
1086
+ for k, v in item.items():
1087
+ lines.append(f" {k}: {v}")
1088
+ else:
1089
+ lines.append(f" {item}")
1090
+
1091
+ lines.append(f"\n{'=' * 70}")
1092
+ lines.append(" RelatΓ³rio gerado pelo Site Error Checker System")
1093
+ lines.append("=" * 70)
1094
+
1095
+ return "\n".join(lines)
1096
+
1097
+ def generate_report_json(self):
1098
+ """Gera relatΓ³rio em formato JSON."""
1099
+ return {
1100
+ "url": self.url,
1101
+ "domain": self.domain,
1102
+ "date": time.strftime('%d/%m/%Y %H:%M:%S'),
1103
+ "total_errors": self.total_errors,
1104
+ "total_warnings": self.total_warnings,
1105
+ "details": self.errors
1106
+ }