dash-devtools 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. dash_devtools/__init__.py +8 -0
  2. dash_devtools/__main__.py +11 -0
  3. dash_devtools/ai_engine.py +441 -0
  4. dash_devtools/browser.py +541 -0
  5. dash_devtools/cli.py +1452 -0
  6. dash_devtools/database.py +338 -0
  7. dash_devtools/dbdiagram.py +183 -0
  8. dash_devtools/e2e.py +329 -0
  9. dash_devtools/fixers/__init__.py +57 -0
  10. dash_devtools/fixers/migration_fixer.py +115 -0
  11. dash_devtools/fixers/ux_fixer.py +106 -0
  12. dash_devtools/fixers/version_bumper.py +115 -0
  13. dash_devtools/gas_mes_test.py +1241 -0
  14. dash_devtools/generators/__init__.py +84 -0
  15. dash_devtools/health.py +476 -0
  16. dash_devtools/hooks/__init__.py +250 -0
  17. dash_devtools/hooks/pre_commit.py +161 -0
  18. dash_devtools/hooks/pre_push.py +275 -0
  19. dash_devtools/init_test.py +352 -0
  20. dash_devtools/markdown_report.py +309 -0
  21. dash_devtools/migrators/__init__.py +21 -0
  22. dash_devtools/perf.py +321 -0
  23. dash_devtools/report.py +667 -0
  24. dash_devtools/reporters/__init__.py +11 -0
  25. dash_devtools/spec.py +230 -0
  26. dash_devtools/stats.py +355 -0
  27. dash_devtools/test_suite.py +690 -0
  28. dash_devtools/testing.py +416 -0
  29. dash_devtools/validators/__init__.py +157 -0
  30. dash_devtools/validators/backend/__init__.py +12 -0
  31. dash_devtools/validators/backend/nodejs.py +245 -0
  32. dash_devtools/validators/backend/python.py +439 -0
  33. dash_devtools/validators/code_quality.py +243 -0
  34. dash_devtools/validators/common/__init__.py +11 -0
  35. dash_devtools/validators/common/quality.py +319 -0
  36. dash_devtools/validators/common/security.py +270 -0
  37. dash_devtools/validators/common/spec.py +273 -0
  38. dash_devtools/validators/detector.py +394 -0
  39. dash_devtools/validators/frontend/__init__.py +14 -0
  40. dash_devtools/validators/frontend/angular.py +245 -0
  41. dash_devtools/validators/frontend/gas.py +310 -0
  42. dash_devtools/validators/frontend/vite.py +539 -0
  43. dash_devtools/validators/migration.py +292 -0
  44. dash_devtools/validators/performance.py +167 -0
  45. dash_devtools/validators/security.py +205 -0
  46. dash_devtools/vision/__init__.py +368 -0
  47. dash_devtools/watch.py +266 -0
  48. dash_devtools/word_report.py +690 -0
  49. dash_devtools-1.0.0.dist-info/METADATA +834 -0
  50. dash_devtools-1.0.0.dist-info/RECORD +53 -0
  51. dash_devtools-1.0.0.dist-info/WHEEL +5 -0
  52. dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
  53. dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,541 @@
1
+ """
2
+ 瀏覽器自動化模組
3
+ 使用 agent-browser 進行網頁操作、截圖、表單填寫等
4
+
5
+ 安裝方式:
6
+ npm install -g agent-browser
7
+ agent-browser install
8
+ """
9
+
10
+ import subprocess
11
+ import json
12
+ import shutil
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Any
15
+ from dataclasses import dataclass
16
+
17
+
18
+ @dataclass
19
+ class BrowserResult:
20
+ """瀏覽器操作結果"""
21
+ success: bool
22
+ output: str = ""
23
+ error: str = ""
24
+ data: Optional[Any] = None
25
+
26
+
27
+ class AgentBrowser:
28
+ """agent-browser CLI wrapper"""
29
+
30
+ def __init__(self, session: Optional[str] = None, headed: bool = False):
31
+ """
32
+ 初始化瀏覽器
33
+
34
+ Args:
35
+ session: 會話名稱 (可同時操作多個瀏覽器)
36
+ headed: 是否顯示瀏覽器視窗 (debug 用)
37
+ """
38
+ self.session = session
39
+ self.headed = headed
40
+ self._check_installed()
41
+
42
+ def _check_installed(self) -> bool:
43
+ """檢查 agent-browser 是否已安裝"""
44
+ if not shutil.which('agent-browser'):
45
+ raise RuntimeError(
46
+ "agent-browser 未安裝。請執行:\n"
47
+ " npm install -g agent-browser\n"
48
+ " agent-browser install"
49
+ )
50
+ return True
51
+
52
+ def _run(self, *args, timeout: int = 30) -> BrowserResult:
53
+ """執行 agent-browser 指令"""
54
+ cmd = ['agent-browser']
55
+
56
+ if self.session:
57
+ cmd.extend(['--session', self.session])
58
+
59
+ cmd.extend(args)
60
+
61
+ try:
62
+ result = subprocess.run(
63
+ cmd,
64
+ capture_output=True,
65
+ text=True,
66
+ timeout=timeout
67
+ )
68
+
69
+ if result.returncode == 0:
70
+ return BrowserResult(
71
+ success=True,
72
+ output=result.stdout.strip()
73
+ )
74
+ else:
75
+ return BrowserResult(
76
+ success=False,
77
+ output=result.stdout.strip(),
78
+ error=result.stderr.strip()
79
+ )
80
+
81
+ except subprocess.TimeoutExpired:
82
+ return BrowserResult(
83
+ success=False,
84
+ error=f"操作超時 ({timeout}秒)"
85
+ )
86
+ except Exception as e:
87
+ return BrowserResult(
88
+ success=False,
89
+ error=str(e)
90
+ )
91
+
92
+ def _run_json(self, *args, timeout: int = 30) -> BrowserResult:
93
+ """執行指令並解析 JSON 輸出"""
94
+ result = self._run(*args, '--json', timeout=timeout)
95
+ if result.success and result.output:
96
+ try:
97
+ result.data = json.loads(result.output)
98
+ except json.JSONDecodeError:
99
+ pass
100
+ return result
101
+
102
+ # ========== 導航操作 ==========
103
+
104
+ def open(self, url: str, timeout: int = 30) -> BrowserResult:
105
+ """開啟網頁"""
106
+ args = ['open', url]
107
+ if self.headed:
108
+ args.append('--headed')
109
+ return self._run(*args, timeout=timeout)
110
+
111
+ def back(self) -> BrowserResult:
112
+ """返回上一頁"""
113
+ return self._run('back')
114
+
115
+ def forward(self) -> BrowserResult:
116
+ """前進下一頁"""
117
+ return self._run('forward')
118
+
119
+ def reload(self) -> BrowserResult:
120
+ """重新載入頁面"""
121
+ return self._run('reload')
122
+
123
+ def close(self) -> BrowserResult:
124
+ """關閉瀏覽器"""
125
+ return self._run('close')
126
+
127
+ # ========== 頁面分析 ==========
128
+
129
+ def snapshot(self, interactive_only: bool = True, compact: bool = False, depth: int = 0) -> BrowserResult:
130
+ """
131
+ 取得頁面快照 (元素列表)
132
+
133
+ Args:
134
+ interactive_only: 只顯示可互動元素
135
+ compact: 精簡輸出
136
+ depth: 限制深度 (0 = 不限)
137
+ """
138
+ args = ['snapshot']
139
+ if interactive_only:
140
+ args.append('-i')
141
+ if compact:
142
+ args.append('-c')
143
+ if depth > 0:
144
+ args.extend(['-d', str(depth)])
145
+
146
+ return self._run_json(*args)
147
+
148
+ def get_title(self) -> str:
149
+ """取得頁面標題"""
150
+ result = self._run('get', 'title')
151
+ return result.output if result.success else ""
152
+
153
+ def get_url(self) -> str:
154
+ """取得目前網址"""
155
+ result = self._run('get', 'url')
156
+ return result.output if result.success else ""
157
+
158
+ def get_text(self, ref: str) -> str:
159
+ """取得元素文字"""
160
+ result = self._run('get', 'text', ref)
161
+ return result.output if result.success else ""
162
+
163
+ def get_value(self, ref: str) -> str:
164
+ """取得輸入框值"""
165
+ result = self._run('get', 'value', ref)
166
+ return result.output if result.success else ""
167
+
168
+ # ========== 互動操作 ==========
169
+
170
+ def click(self, ref: str) -> BrowserResult:
171
+ """點擊元素"""
172
+ return self._run('click', ref)
173
+
174
+ def dblclick(self, ref: str) -> BrowserResult:
175
+ """雙擊元素"""
176
+ return self._run('dblclick', ref)
177
+
178
+ def fill(self, ref: str, text: str) -> BrowserResult:
179
+ """填寫輸入框 (會先清空)"""
180
+ return self._run('fill', ref, text)
181
+
182
+ def type_text(self, ref: str, text: str) -> BrowserResult:
183
+ """輸入文字 (不清空)"""
184
+ return self._run('type', ref, text)
185
+
186
+ def press(self, key: str) -> BrowserResult:
187
+ """按鍵 (如 Enter, Tab, Control+a)"""
188
+ return self._run('press', key)
189
+
190
+ def hover(self, ref: str) -> BrowserResult:
191
+ """滑鼠懸停"""
192
+ return self._run('hover', ref)
193
+
194
+ def check(self, ref: str) -> BrowserResult:
195
+ """勾選 checkbox"""
196
+ return self._run('check', ref)
197
+
198
+ def uncheck(self, ref: str) -> BrowserResult:
199
+ """取消勾選 checkbox"""
200
+ return self._run('uncheck', ref)
201
+
202
+ def select(self, ref: str, value: str) -> BrowserResult:
203
+ """選擇下拉選單"""
204
+ return self._run('select', ref, value)
205
+
206
+ def scroll(self, direction: str = 'down', amount: int = 500) -> BrowserResult:
207
+ """捲動頁面"""
208
+ return self._run('scroll', direction, str(amount))
209
+
210
+ def scroll_to(self, ref: str) -> BrowserResult:
211
+ """捲動到元素"""
212
+ return self._run('scrollintoview', ref)
213
+
214
+ # ========== 截圖 ==========
215
+
216
+ def screenshot(self, path: Optional[str] = None, full_page: bool = False) -> BrowserResult:
217
+ """
218
+ 截圖
219
+
220
+ Args:
221
+ path: 儲存路徑 (None 則輸出到 stdout)
222
+ full_page: 是否截取完整頁面
223
+ """
224
+ args = ['screenshot']
225
+ if path:
226
+ args.append(path)
227
+ if full_page:
228
+ args.append('--full')
229
+
230
+ return self._run(*args, timeout=60)
231
+
232
+ # ========== 等待 ==========
233
+
234
+ def wait(self, ref_or_ms: str, timeout: int = 30) -> BrowserResult:
235
+ """等待元素或時間"""
236
+ return self._run('wait', ref_or_ms, timeout=timeout)
237
+
238
+ def wait_text(self, text: str, timeout: int = 30) -> BrowserResult:
239
+ """等待文字出現"""
240
+ return self._run('wait', '--text', text, timeout=timeout)
241
+
242
+ def wait_load(self, state: str = 'networkidle', timeout: int = 30) -> BrowserResult:
243
+ """等待載入完成 (networkidle, domcontentloaded, load)"""
244
+ return self._run('wait', '--load', state, timeout=timeout)
245
+
246
+ def wait_url(self, pattern: str, timeout: int = 30) -> BrowserResult:
247
+ """等待網址符合 pattern"""
248
+ return self._run('wait', '--url', pattern, timeout=timeout)
249
+
250
+ # ========== 狀態管理 ==========
251
+
252
+ def state_save(self, path: str) -> BrowserResult:
253
+ """儲存登入狀態 (cookies, localStorage)"""
254
+ return self._run('state', 'save', path)
255
+
256
+ def state_load(self, path: str) -> BrowserResult:
257
+ """載入登入狀態"""
258
+ return self._run('state', 'load', path)
259
+
260
+ # ========== 語意定位 ==========
261
+
262
+ def find_and_click(self, locator_type: str, value: str, **kwargs) -> BrowserResult:
263
+ """
264
+ 使用語意定位點擊
265
+
266
+ Args:
267
+ locator_type: role, text, label
268
+ value: 定位值
269
+ **kwargs: 額外參數 (如 name)
270
+ """
271
+ args = ['find', locator_type, value, 'click']
272
+ for k, v in kwargs.items():
273
+ args.extend([f'--{k}', str(v)])
274
+ return self._run(*args)
275
+
276
+ def find_and_fill(self, locator_type: str, value: str, text: str, **kwargs) -> BrowserResult:
277
+ """使用語意定位填寫"""
278
+ args = ['find', locator_type, value, 'fill', text]
279
+ for k, v in kwargs.items():
280
+ args.extend([f'--{k}', str(v)])
281
+ return self._run(*args)
282
+
283
+ # ========== 進階互動 ==========
284
+
285
+ def focus(self, ref: str) -> BrowserResult:
286
+ """聚焦元素"""
287
+ return self._run('focus', ref)
288
+
289
+ def drag(self, from_ref: str, to_ref: str) -> BrowserResult:
290
+ """拖放元素"""
291
+ return self._run('drag', from_ref, to_ref)
292
+
293
+ def upload(self, ref: str, file_path: str) -> BrowserResult:
294
+ """上傳檔案"""
295
+ return self._run('upload', ref, file_path)
296
+
297
+ # ========== PDF ==========
298
+
299
+ def pdf(self, path: str) -> BrowserResult:
300
+ """輸出 PDF"""
301
+ return self._run('pdf', path, timeout=60)
302
+
303
+ # ========== Cookie 管理 ==========
304
+
305
+ def cookies_get(self) -> BrowserResult:
306
+ """取得所有 cookies"""
307
+ return self._run_json('cookies')
308
+
309
+ def cookies_set(self, name: str, value: str, **kwargs) -> BrowserResult:
310
+ """設定 cookie"""
311
+ args = ['cookies', 'set', name, value]
312
+ for k, v in kwargs.items():
313
+ args.extend([f'--{k}', str(v)])
314
+ return self._run(*args)
315
+
316
+ def cookies_clear(self) -> BrowserResult:
317
+ """清除所有 cookies"""
318
+ return self._run('cookies', 'clear')
319
+
320
+ # ========== JavaScript 執行 ==========
321
+
322
+ def evaluate(self, js_code: str) -> BrowserResult:
323
+ """執行 JavaScript"""
324
+ return self._run('evaluate', js_code)
325
+
326
+ # ========== 網路請求 ==========
327
+
328
+ def network(self) -> BrowserResult:
329
+ """取得網路請求列表"""
330
+ return self._run_json('network')
331
+
332
+ # ========== 會話管理 ==========
333
+
334
+ def session_list(self) -> BrowserResult:
335
+ """列出所有會話"""
336
+ return self._run_json('session', 'list')
337
+
338
+ def session_kill(self, session_name: str) -> BrowserResult:
339
+ """結束指定會話"""
340
+ return self._run('session', 'kill', session_name)
341
+
342
+ # ========== Debug ==========
343
+
344
+ def console(self) -> BrowserResult:
345
+ """取得 console 訊息"""
346
+ return self._run('console')
347
+
348
+ def errors(self) -> BrowserResult:
349
+ """取得頁面錯誤"""
350
+ return self._run('errors')
351
+
352
+
353
+ # ========== 便捷函數 ==========
354
+
355
+ def quick_screenshot(url: str, output_path: str, wait_ms: int = 3000, mobile: bool = False) -> bool:
356
+ """
357
+ 快速截圖
358
+
359
+ Args:
360
+ url: 網址
361
+ output_path: 輸出路徑
362
+ wait_ms: 等待時間 (毫秒)
363
+ mobile: 是否使用手機版視窗
364
+
365
+ Returns:
366
+ 是否成功
367
+ """
368
+ browser = AgentBrowser()
369
+ try:
370
+ result = browser.open(url)
371
+ if not result.success:
372
+ return False
373
+
374
+ # 等待頁面載入
375
+ browser.wait(str(wait_ms))
376
+
377
+ # 截圖
378
+ result = browser.screenshot(output_path, full_page=True)
379
+ return result.success
380
+
381
+ finally:
382
+ browser.close()
383
+
384
+
385
+ def check_page_errors(url: str, timeout: int = 30) -> Dict:
386
+ """
387
+ 檢查頁面 JS 錯誤
388
+
389
+ Args:
390
+ url: 網址
391
+ timeout: 超時時間 (秒)
392
+
393
+ Returns:
394
+ {
395
+ 'success': bool,
396
+ 'url': str,
397
+ 'title': str,
398
+ 'errors': List[str],
399
+ 'load_time': int
400
+ }
401
+ """
402
+ import time
403
+ browser = AgentBrowser()
404
+ result = {
405
+ 'success': True,
406
+ 'url': url,
407
+ 'title': '',
408
+ 'errors': [],
409
+ 'load_time': 0
410
+ }
411
+
412
+ try:
413
+ start = time.time()
414
+ open_result = browser.open(url, timeout=timeout)
415
+ result['load_time'] = int((time.time() - start) * 1000)
416
+
417
+ if not open_result.success:
418
+ result['success'] = False
419
+ result['errors'].append(open_result.error or "頁面載入失敗")
420
+ return result
421
+
422
+ # 等待 JS 執行
423
+ browser.wait('2000')
424
+
425
+ # 取得標題
426
+ result['title'] = browser.get_title()
427
+
428
+ # 取得錯誤
429
+ errors_result = browser.errors()
430
+ if errors_result.output:
431
+ # 解析錯誤訊息
432
+ error_lines = [
433
+ line.strip()
434
+ for line in errors_result.output.split('\n')
435
+ if line.strip() and 'favicon' not in line.lower()
436
+ ]
437
+ result['errors'] = error_lines
438
+
439
+ result['success'] = len(result['errors']) == 0
440
+
441
+ except Exception as e:
442
+ result['success'] = False
443
+ result['errors'].append(str(e))
444
+
445
+ finally:
446
+ browser.close()
447
+
448
+ return result
449
+
450
+
451
+ def fill_form(url: str, fields: Dict[str, str], submit_ref: Optional[str] = None) -> BrowserResult:
452
+ """
453
+ 填寫表單
454
+
455
+ Args:
456
+ url: 網址
457
+ fields: {ref: value} 對應
458
+ submit_ref: 送出按鈕 ref (可選)
459
+
460
+ Returns:
461
+ 操作結果
462
+ """
463
+ browser = AgentBrowser()
464
+ try:
465
+ result = browser.open(url)
466
+ if not result.success:
467
+ return result
468
+
469
+ browser.wait('2000')
470
+
471
+ # 填寫欄位
472
+ for ref, value in fields.items():
473
+ result = browser.fill(ref, value)
474
+ if not result.success:
475
+ return result
476
+
477
+ # 送出表單
478
+ if submit_ref:
479
+ result = browser.click(submit_ref)
480
+ browser.wait_load('networkidle')
481
+
482
+ return BrowserResult(success=True, output="表單填寫完成")
483
+
484
+ finally:
485
+ browser.close()
486
+
487
+
488
+ def login_and_save_state(
489
+ login_url: str,
490
+ username_ref: str,
491
+ password_ref: str,
492
+ submit_ref: str,
493
+ username: str,
494
+ password: str,
495
+ state_file: str,
496
+ success_url_pattern: Optional[str] = None
497
+ ) -> BrowserResult:
498
+ """
499
+ 登入並儲存狀態
500
+
501
+ Args:
502
+ login_url: 登入頁面網址
503
+ username_ref: 帳號輸入框 ref
504
+ password_ref: 密碼輸入框 ref
505
+ submit_ref: 登入按鈕 ref
506
+ username: 帳號
507
+ password: 密碼
508
+ state_file: 狀態儲存檔案
509
+ success_url_pattern: 登入成功後的網址 pattern
510
+
511
+ Returns:
512
+ 操作結果
513
+ """
514
+ browser = AgentBrowser()
515
+ try:
516
+ result = browser.open(login_url)
517
+ if not result.success:
518
+ return result
519
+
520
+ browser.wait('2000')
521
+
522
+ # 填寫登入資訊
523
+ browser.fill(username_ref, username)
524
+ browser.fill(password_ref, password)
525
+ browser.click(submit_ref)
526
+
527
+ # 等待登入完成
528
+ if success_url_pattern:
529
+ browser.wait_url(success_url_pattern, timeout=30)
530
+ else:
531
+ browser.wait_load('networkidle')
532
+
533
+ # 儲存狀態
534
+ result = browser.state_save(state_file)
535
+ if result.success:
536
+ return BrowserResult(success=True, output=f"登入成功,狀態已儲存至 {state_file}")
537
+ else:
538
+ return result
539
+
540
+ finally:
541
+ browser.close()