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,690 @@
1
+ """
2
+ Word 測試報告生成模組
3
+
4
+ 生成包含圖文內容的專業測試報告
5
+ - 封面頁
6
+ - 測試摘要
7
+ - 測試結果表格
8
+ - 圖表 (通過率圓餅圖、各類型長條圖)
9
+ - 截圖 (可選)
10
+ """
11
+
12
+ import io
13
+ import tempfile
14
+ from pathlib import Path
15
+ from datetime import datetime
16
+ from typing import Dict, List, Optional
17
+
18
+ try:
19
+ from docx import Document
20
+ from docx.shared import Inches, Pt, Cm, RGBColor
21
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
22
+ from docx.enum.table import WD_TABLE_ALIGNMENT
23
+ from docx.enum.style import WD_STYLE_TYPE
24
+ from docx.oxml.ns import qn
25
+ from docx.oxml import OxmlElement
26
+ HAS_DOCX = True
27
+ except ImportError:
28
+ HAS_DOCX = False
29
+
30
+ try:
31
+ import matplotlib
32
+ matplotlib.use('Agg') # 非互動模式
33
+ import matplotlib.pyplot as plt
34
+ HAS_MATPLOTLIB = True
35
+ except ImportError:
36
+ HAS_MATPLOTLIB = False
37
+
38
+ from rich.console import Console
39
+
40
+ console = Console()
41
+
42
+
43
+ def set_cell_shading(cell, color: str):
44
+ """設定表格儲存格背景色"""
45
+ shading = OxmlElement('w:shd')
46
+ shading.set(qn('w:fill'), color)
47
+ cell._tc.get_or_add_tcPr().append(shading)
48
+
49
+
50
+ def create_pass_rate_chart(passed: int, failed: int) -> Optional[bytes]:
51
+ """建立通過率圓餅圖"""
52
+ if not HAS_MATPLOTLIB:
53
+ return None
54
+
55
+ # 設定中文字型 (macOS)
56
+ plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
57
+ plt.rcParams['axes.unicode_minus'] = False
58
+
59
+ fig, ax = plt.subplots(figsize=(4, 4))
60
+
61
+ if passed + failed == 0:
62
+ sizes = [1]
63
+ colors = ['#E0E0E0']
64
+ labels = ['無測試']
65
+ else:
66
+ sizes = [passed, failed] if failed > 0 else [passed]
67
+ colors = ['#4CAF50', '#F44336'] if failed > 0 else ['#4CAF50']
68
+ labels = ['通過', '失敗'] if failed > 0 else ['通過']
69
+
70
+ wedges, texts, autotexts = ax.pie(
71
+ sizes,
72
+ labels=labels,
73
+ colors=colors,
74
+ autopct='%1.1f%%',
75
+ startangle=90,
76
+ textprops={'fontsize': 12}
77
+ )
78
+
79
+ ax.set_title('測試通過率', fontsize=14, fontweight='bold')
80
+
81
+ # 儲存為 bytes
82
+ buf = io.BytesIO()
83
+ plt.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white')
84
+ plt.close(fig)
85
+ buf.seek(0)
86
+ return buf.read()
87
+
88
+
89
+ def create_test_type_chart(results: Dict) -> Optional[bytes]:
90
+ """建立各類型測試長條圖"""
91
+ if not HAS_MATPLOTLIB:
92
+ return None
93
+
94
+ # 過濾掉未設定的測試
95
+ configured_results = {k: v for k, v in results.items() if not v.get('not_configured', False)}
96
+ if not configured_results:
97
+ return None
98
+
99
+ # 設定中文字型
100
+ plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'Microsoft JhengHei']
101
+ plt.rcParams['axes.unicode_minus'] = False
102
+
103
+ fig, ax = plt.subplots(figsize=(8, 4))
104
+
105
+ types = list(configured_results.keys())
106
+ passed = [configured_results[t].get('passed', 0) for t in types]
107
+ failed = [configured_results[t].get('failed', 0) for t in types]
108
+
109
+ x = range(len(types))
110
+ width = 0.35
111
+
112
+ bars1 = ax.bar([i - width/2 for i in x], passed, width, label='通過', color='#4CAF50')
113
+ bars2 = ax.bar([i + width/2 for i in x], failed, width, label='失敗', color='#F44336')
114
+
115
+ ax.set_ylabel('測試數量')
116
+ ax.set_title('各類型測試結果', fontsize=14, fontweight='bold')
117
+ ax.set_xticks(x)
118
+ ax.set_xticklabels(types)
119
+ ax.legend()
120
+
121
+ # 加上數值標籤
122
+ for bar in bars1:
123
+ height = bar.get_height()
124
+ if height > 0:
125
+ ax.annotate(f'{int(height)}',
126
+ xy=(bar.get_x() + bar.get_width() / 2, height),
127
+ xytext=(0, 3),
128
+ textcoords="offset points",
129
+ ha='center', va='bottom', fontsize=10)
130
+
131
+ for bar in bars2:
132
+ height = bar.get_height()
133
+ if height > 0:
134
+ ax.annotate(f'{int(height)}',
135
+ xy=(bar.get_x() + bar.get_width() / 2, height),
136
+ xytext=(0, 3),
137
+ textcoords="offset points",
138
+ ha='center', va='bottom', fontsize=10)
139
+
140
+ plt.tight_layout()
141
+
142
+ buf = io.BytesIO()
143
+ plt.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white')
144
+ plt.close(fig)
145
+ buf.seek(0)
146
+ return buf.read()
147
+
148
+
149
+ def generate_word_report(
150
+ project_name: str,
151
+ test_results: Dict,
152
+ output_path: str,
153
+ screenshots: List[str] = None,
154
+ include_charts: bool = True
155
+ ) -> str:
156
+ """
157
+ 生成 Word 測試報告
158
+
159
+ Args:
160
+ project_name: 專案名稱
161
+ test_results: 測試結果字典 (來自 test_suite)
162
+ output_path: 輸出檔案路徑
163
+ screenshots: 截圖路徑列表 (可選)
164
+ include_charts: 是否包含圖表
165
+
166
+ Returns:
167
+ 輸出檔案路徑
168
+ """
169
+ if not HAS_DOCX:
170
+ raise ImportError("請安裝 python-docx: pip install python-docx")
171
+
172
+ doc = Document()
173
+
174
+ # 設定文件樣式
175
+ style = doc.styles['Normal']
176
+ style.font.name = 'Microsoft JhengHei'
177
+ style.font.size = Pt(12)
178
+
179
+ # ========== 封面頁 ==========
180
+ # 標題
181
+ title = doc.add_heading('', level=0)
182
+ title_run = title.add_run(f'{project_name} 測試報告')
183
+ title_run.font.size = Pt(36)
184
+ title_run.font.bold = True
185
+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER
186
+
187
+ # 副標題
188
+ subtitle = doc.add_paragraph()
189
+ subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
190
+ sub_run = subtitle.add_run('四大類型測試套件執行結果')
191
+ sub_run.font.size = Pt(18)
192
+ sub_run.font.color.rgb = RGBColor(100, 100, 100)
193
+
194
+ # 日期
195
+ doc.add_paragraph()
196
+ date_para = doc.add_paragraph()
197
+ date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
198
+ timestamp = test_results.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
199
+ date_run = date_para.add_run(f'報告產生時間: {timestamp}')
200
+ date_run.font.size = Pt(14)
201
+
202
+ # 狀態標示
203
+ doc.add_paragraph()
204
+ doc.add_paragraph()
205
+ status_para = doc.add_paragraph()
206
+ status_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
207
+
208
+ overall_success = test_results.get('overall_success', True)
209
+ if overall_success:
210
+ status_run = status_para.add_run('ALL TESTS PASSED')
211
+ status_run.font.size = Pt(24)
212
+ status_run.font.bold = True
213
+ status_run.font.color.rgb = RGBColor(76, 175, 80) # 綠色
214
+ else:
215
+ status_run = status_para.add_run('SOME TESTS FAILED')
216
+ status_run.font.size = Pt(24)
217
+ status_run.font.bold = True
218
+ status_run.font.color.rgb = RGBColor(244, 67, 54) # 紅色
219
+
220
+ doc.add_page_break()
221
+
222
+ # ========== 測試摘要 ==========
223
+ doc.add_heading('測試摘要', level=1)
224
+
225
+ summary = test_results.get('summary', {})
226
+ total_passed = summary.get('total_passed', 0)
227
+ total_failed = summary.get('total_failed', 0)
228
+ total_duration = summary.get('total_duration', 0)
229
+ coverage = summary.get('coverage', 0)
230
+
231
+ # 如果 total_duration 為 0,從各測試類型的 test_cases 計算
232
+ if total_duration == 0:
233
+ tests = test_results.get('tests', {})
234
+ for result in tests.values():
235
+ test_cases = result.get('test_cases', [])
236
+ total_duration += sum(tc.get('duration', 0) for tc in test_cases)
237
+
238
+ # 摘要表格
239
+ summary_table = doc.add_table(rows=5, cols=2)
240
+ summary_table.style = 'Table Grid'
241
+
242
+ # 智慧格式化總執行時間
243
+ if total_duration <= 0:
244
+ duration_str = '-'
245
+ elif total_duration < 0.001: # < 1ms
246
+ duration_str = f'{total_duration * 1000000:.0f}us'
247
+ elif total_duration < 0.1: # < 100ms
248
+ duration_str = f'{total_duration * 1000:.2f}ms'
249
+ elif total_duration < 1: # < 1s
250
+ duration_str = f'{total_duration * 1000:.0f}ms'
251
+ else:
252
+ duration_str = f'{total_duration:.1f}s'
253
+
254
+ summary_data = [
255
+ ('總測試數', str(total_passed + total_failed)),
256
+ ('通過', str(total_passed)),
257
+ ('失敗', str(total_failed)),
258
+ ('執行時間', duration_str),
259
+ ('程式碼覆蓋率', f'{coverage:.1f}%' if coverage > 0 else 'N/A'),
260
+ ]
261
+
262
+ for i, (label, value) in enumerate(summary_data):
263
+ row = summary_table.rows[i]
264
+ row.cells[0].text = label
265
+ row.cells[1].text = value
266
+ # 設定標籤欄背景色
267
+ set_cell_shading(row.cells[0], 'F5F5F5')
268
+
269
+ doc.add_paragraph()
270
+
271
+ # ========== 通過率圖表 ==========
272
+ if include_charts and HAS_MATPLOTLIB:
273
+ doc.add_heading('測試通過率', level=2)
274
+
275
+ chart_data = create_pass_rate_chart(total_passed, total_failed)
276
+ if chart_data:
277
+ with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
278
+ tmp.write(chart_data)
279
+ tmp_path = tmp.name
280
+
281
+ doc.add_picture(tmp_path, width=Inches(3.5))
282
+ last_paragraph = doc.paragraphs[-1]
283
+ last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
284
+
285
+ Path(tmp_path).unlink(missing_ok=True)
286
+
287
+ doc.add_paragraph()
288
+
289
+ # ========== 各類型測試結果 ==========
290
+ doc.add_heading('各類型測試結果', level=1)
291
+
292
+ tests = test_results.get('tests', {})
293
+
294
+ # 過濾掉未設定的測試
295
+ configured_tests = {k: v for k, v in tests.items() if not v.get('not_configured', False)}
296
+
297
+ # 如果沒有任何已設定的測試
298
+ if not configured_tests:
299
+ p = doc.add_paragraph()
300
+ run = p.add_run('此專案未設定任何測試框架')
301
+ run.font.color.rgb = RGBColor(128, 128, 128)
302
+ run.italic = True
303
+ else:
304
+ # 結果表格
305
+ result_table = doc.add_table(rows=len(configured_tests) + 1, cols=5)
306
+ result_table.style = 'Table Grid'
307
+
308
+ # 表頭
309
+ header_row = result_table.rows[0]
310
+ headers = ['測試類型', '狀態', '通過', '失敗', '時間']
311
+ for i, header in enumerate(headers):
312
+ cell = header_row.cells[i]
313
+ cell.text = header
314
+ set_cell_shading(cell, '2196F3')
315
+ for paragraph in cell.paragraphs:
316
+ for run in paragraph.runs:
317
+ run.font.bold = True
318
+ run.font.color.rgb = RGBColor(255, 255, 255)
319
+
320
+ # 資料列
321
+ type_labels = {
322
+ 'UIT': '單元測試 (UIT)',
323
+ 'SMOKE': '煙霧測試 (Smoke)',
324
+ 'E2E': '端對端測試 (E2E)',
325
+ 'UAT': '驗收測試 (UAT)'
326
+ }
327
+
328
+ for i, (test_type, result) in enumerate(configured_tests.items(), start=1):
329
+ row = result_table.rows[i]
330
+ row.cells[0].text = type_labels.get(test_type, test_type)
331
+
332
+ success = result.get('success', True)
333
+ row.cells[1].text = 'PASS' if success else 'FAIL'
334
+ # 狀態顏色
335
+ status_cell = row.cells[1]
336
+ if success:
337
+ set_cell_shading(status_cell, 'C8E6C9') # 淺綠
338
+ else:
339
+ set_cell_shading(status_cell, 'FFCDD2') # 淺紅
340
+
341
+ row.cells[2].text = str(result.get('passed', 0))
342
+ row.cells[3].text = str(result.get('failed', 0))
343
+ # 計算總時間 (從 test_cases 或 result.duration)
344
+ duration = result.get('duration', 0)
345
+ if duration == 0:
346
+ test_cases = result.get('test_cases', [])
347
+ duration = sum(tc.get('duration', 0) for tc in test_cases)
348
+ # 智慧格式化 (所有單位都是秒,< 1s 顯示 ms)
349
+ if duration <= 0:
350
+ row.cells[4].text = "-"
351
+ elif duration < 0.1: # < 100ms 顯示 ms
352
+ row.cells[4].text = f"{duration * 1000:.2f}ms"
353
+ elif duration < 1: # < 1s 顯示 ms (整數)
354
+ row.cells[4].text = f"{duration * 1000:.0f}ms"
355
+ else:
356
+ row.cells[4].text = f"{duration:.1f}s"
357
+
358
+ doc.add_paragraph()
359
+
360
+ # ========== 詳細測試案例列表 (含截圖) ==========
361
+ for test_type, result in configured_tests.items():
362
+ test_cases = result.get('test_cases', [])
363
+ if not test_cases:
364
+ continue
365
+
366
+ doc.add_page_break()
367
+ doc.add_heading(f'{type_labels.get(test_type, test_type)} - 測試案例明細', level=1)
368
+
369
+ for i, tc in enumerate(test_cases, start=1):
370
+ test_name = tc.get('name', '')
371
+ status = tc.get('status', 'passed')
372
+ duration = tc.get('duration', 0)
373
+ screenshot_path = tc.get('screenshot', '')
374
+
375
+ # 測試案例標題
376
+ p = doc.add_paragraph()
377
+ # 狀態圖示
378
+ if status == 'passed':
379
+ status_run = p.add_run('[PASS] ')
380
+ status_run.font.color.rgb = RGBColor(76, 175, 80)
381
+ status_run.bold = True
382
+ elif status == 'failed':
383
+ status_run = p.add_run('[FAIL] ')
384
+ status_run.font.color.rgb = RGBColor(244, 67, 54)
385
+ status_run.bold = True
386
+ else:
387
+ status_run = p.add_run('[SKIP] ')
388
+ status_run.font.color.rgb = RGBColor(255, 193, 7)
389
+ status_run.bold = True
390
+
391
+ # 測試名稱
392
+ name_run = p.add_run(f'{i}. {test_name}')
393
+ name_run.font.size = Pt(11)
394
+
395
+ # 時間 (單位: 秒,小於 1 秒顯示毫秒)
396
+ if duration and duration > 0:
397
+ if duration < 0.001: # < 1ms
398
+ time_str = f'{duration * 1000000:.0f}us'
399
+ elif duration < 0.1: # < 100ms
400
+ time_str = f'{duration * 1000:.2f}ms'
401
+ elif duration < 1: # < 1s
402
+ time_str = f'{duration * 1000:.0f}ms'
403
+ else:
404
+ time_str = f'{duration:.2f}s'
405
+ time_run = p.add_run(f' ({time_str})')
406
+ time_run.font.size = Pt(9)
407
+ time_run.font.color.rgb = RGBColor(128, 128, 128)
408
+
409
+ # 截圖
410
+ if screenshot_path and Path(screenshot_path).exists():
411
+ doc.add_picture(screenshot_path, width=Inches(5.5))
412
+ last_paragraph = doc.paragraphs[-1]
413
+ last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
414
+ else:
415
+ # API 回應 (當沒有截圖時顯示)
416
+ api_response = tc.get('api_response', '')
417
+ terminal_output = tc.get('terminal_output', '')
418
+
419
+ if api_response:
420
+ api_label = doc.add_paragraph()
421
+ api_label_run = api_label.add_run('API Response:')
422
+ api_label_run.font.size = Pt(10)
423
+ api_label_run.font.color.rgb = RGBColor(33, 150, 243)
424
+ api_label_run.bold = True
425
+
426
+ # 格式化 JSON 顯示
427
+ api_para = doc.add_paragraph()
428
+ api_para.paragraph_format.left_indent = Inches(0.3)
429
+ # 限制顯示長度
430
+ display_response = api_response[:500] + ('...' if len(api_response) > 500 else '')
431
+ api_run = api_para.add_run(display_response)
432
+ api_run.font.size = Pt(9)
433
+ api_run.font.name = 'Consolas'
434
+ api_run.font.color.rgb = RGBColor(80, 80, 80)
435
+
436
+ elif terminal_output:
437
+ # 終端輸出 (UIT 測試)
438
+ term_label = doc.add_paragraph()
439
+ term_label_run = term_label.add_run('Terminal Output:')
440
+ term_label_run.font.size = Pt(10)
441
+ term_label_run.font.color.rgb = RGBColor(156, 39, 176)
442
+ term_label_run.bold = True
443
+
444
+ term_para = doc.add_paragraph()
445
+ term_para.paragraph_format.left_indent = Inches(0.3)
446
+ display_output = terminal_output[:400] + ('...' if len(terminal_output) > 400 else '')
447
+ term_run = term_para.add_run(display_output)
448
+ term_run.font.size = Pt(9)
449
+ term_run.font.name = 'Consolas'
450
+ term_run.font.color.rgb = RGBColor(80, 80, 80)
451
+
452
+ # 錯誤訊息
453
+ error = tc.get('error', '')
454
+ if error:
455
+ error_p = doc.add_paragraph()
456
+ error_run = error_p.add_run(f'Error: {error[:300]}')
457
+ error_run.font.size = Pt(9)
458
+ error_run.font.color.rgb = RGBColor(244, 67, 54)
459
+
460
+ doc.add_paragraph() # 間隔
461
+
462
+ # ========== 各類型長條圖 ==========
463
+ if include_charts and HAS_MATPLOTLIB and tests:
464
+ doc.add_heading('測試結果分布', level=2)
465
+
466
+ chart_data = create_test_type_chart(tests)
467
+ if chart_data:
468
+ with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
469
+ tmp.write(chart_data)
470
+ tmp_path = tmp.name
471
+
472
+ doc.add_picture(tmp_path, width=Inches(6))
473
+ last_paragraph = doc.paragraphs[-1]
474
+ last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
475
+
476
+ Path(tmp_path).unlink(missing_ok=True)
477
+
478
+ doc.add_page_break()
479
+
480
+ # ========== 測試類型說明 ==========
481
+ doc.add_heading('測試類型說明', level=1)
482
+
483
+ descriptions = [
484
+ ('UIT (Unit Integration Testing)',
485
+ '單元測試驗證各個模組、函數的正確性。使用 Vitest/Jest 框架執行,並產生程式碼覆蓋率報告。'),
486
+ ('Smoke Test (煙霧測試)',
487
+ '快速驗證系統關鍵路徑是否正常運作。包含應用程式啟動、頁面載入、API 健康檢查等基本功能。'),
488
+ ('E2E (End-to-End Testing)',
489
+ '端對端測試模擬真實使用情境,驗證完整的使用者流程。使用 Playwright 自動化測試框架執行。'),
490
+ ('UAT (User Acceptance Testing)',
491
+ '使用者驗收測試從業務角度驗證系統符合需求規格。測試案例依據使用者角色設計,確保系統滿足業務需求。'),
492
+ ]
493
+
494
+ for title, desc in descriptions:
495
+ p = doc.add_paragraph()
496
+ title_run = p.add_run(title + ': ')
497
+ title_run.bold = True
498
+ p.add_run(desc)
499
+ doc.add_paragraph()
500
+
501
+ # ========== 額外截圖 (可選,用於補充說明) ==========
502
+ if screenshots:
503
+ doc.add_page_break()
504
+ doc.add_heading('補充截圖', level=1)
505
+
506
+ for i, screenshot_path in enumerate(screenshots):
507
+ if Path(screenshot_path).exists():
508
+ p = doc.add_paragraph()
509
+ run = p.add_run(f'截圖 {i + 1}')
510
+ run.bold = True
511
+ run.font.size = Pt(12)
512
+
513
+ doc.add_picture(screenshot_path, width=Inches(6))
514
+ last_paragraph = doc.paragraphs[-1]
515
+ last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
516
+ doc.add_paragraph()
517
+
518
+ # ========== 頁尾 ==========
519
+ doc.add_paragraph()
520
+ footer = doc.add_paragraph()
521
+ footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
522
+ footer_run = footer.add_run('Generated by DashAI DevTools')
523
+ footer_run.font.size = Pt(10)
524
+ footer_run.font.color.rgb = RGBColor(150, 150, 150)
525
+
526
+ # 儲存文件
527
+ output_file = Path(output_path)
528
+ doc.save(output_file)
529
+
530
+ return str(output_file)
531
+
532
+
533
+ def take_screenshots(project_path: str, urls: List[str] = None) -> List[str]:
534
+ """
535
+ 使用 Puppeteer 截取頁面截圖
536
+
537
+ Args:
538
+ project_path: 專案路徑
539
+ urls: 要截圖的 URL 列表
540
+
541
+ Returns:
542
+ 截圖檔案路徑列表
543
+ """
544
+ import subprocess
545
+ import tempfile
546
+
547
+ if not urls:
548
+ # 預設截圖頁面
549
+ urls = [
550
+ 'https://smai-mes.vercel.app/',
551
+ 'https://smai-mes.vercel.app/warehouse/transfer',
552
+ 'https://smai-mes.vercel.app/warehouse/line-side',
553
+ ]
554
+
555
+ screenshots = []
556
+ screenshot_dir = Path(tempfile.mkdtemp(prefix='test-screenshots-'))
557
+
558
+ # Puppeteer 截圖腳本
559
+ for i, url in enumerate(urls):
560
+ screenshot_path = screenshot_dir / f'screenshot-{i+1}.png'
561
+ script = f'''
562
+ const puppeteer = require("puppeteer");
563
+ (async () => {{
564
+ const browser = await puppeteer.launch({{ headless: "new" }});
565
+ const page = await browser.newPage();
566
+ await page.setViewport({{ width: 1920, height: 1080 }});
567
+ await page.goto("{url}", {{ waitUntil: "networkidle0", timeout: 30000 }});
568
+ await new Promise(r => setTimeout(r, 2000));
569
+ await page.screenshot({{ path: "{screenshot_path}", fullPage: false }});
570
+ await browser.close();
571
+ }})();
572
+ '''
573
+ try:
574
+ # 找有 puppeteer 的目錄
575
+ puppeteer_dirs = [
576
+ '/Users/dash/Documents/github/smai-process-vision',
577
+ '/Users/dash/Documents/github/MES',
578
+ project_path
579
+ ]
580
+
581
+ for pdir in puppeteer_dirs:
582
+ if (Path(pdir) / 'node_modules' / 'puppeteer').exists():
583
+ result = subprocess.run(
584
+ ['node', '-e', script],
585
+ cwd=pdir,
586
+ capture_output=True,
587
+ timeout=60
588
+ )
589
+ if result.returncode == 0 and screenshot_path.exists():
590
+ screenshots.append(str(screenshot_path))
591
+ console.print(f"[dim] 截圖: {url}[/dim]")
592
+ break
593
+ except Exception as e:
594
+ console.print(f"[yellow]截圖失敗: {url} - {e}[/yellow]")
595
+
596
+ return screenshots
597
+
598
+
599
+ def run_and_generate_report(
600
+ project_path: str,
601
+ output_path: str = None,
602
+ test_types: List[str] = None,
603
+ include_screenshots: bool = True,
604
+ screenshot_urls: List[str] = None
605
+ ) -> Dict:
606
+ """
607
+ 執行測試並生成 Word 報告
608
+
609
+ Args:
610
+ project_path: 專案路徑
611
+ output_path: 輸出路徑 (預設為專案目錄下的 test-report.docx)
612
+ test_types: 測試類型列表
613
+ include_screenshots: 是否包含截圖 (預設 True)
614
+ screenshot_urls: 要截圖的 URL 列表
615
+
616
+ Returns:
617
+ 結果字典
618
+ """
619
+ from .test_suite import TestSuiteRunner
620
+
621
+ project = Path(project_path).resolve() # 使用 resolve() 取得完整路徑
622
+ project_name = project.name
623
+
624
+ if not output_path:
625
+ output_path = str(project / f'{project_name}-test-report.docx')
626
+
627
+ # 執行測試
628
+ console.print(f"[cyan]執行 {project_name} 測試套件...[/cyan]")
629
+ runner = TestSuiteRunner(project_path)
630
+ suite_result = runner.run_all(test_types)
631
+
632
+ # 準備測試結果 (包含測試案例明細)
633
+ test_results = {
634
+ 'project': project_name,
635
+ 'timestamp': suite_result.timestamp,
636
+ 'overall_success': suite_result.overall_success,
637
+ 'summary': {
638
+ 'total_passed': suite_result.total_passed,
639
+ 'total_failed': suite_result.total_failed,
640
+ 'total_duration': suite_result.total_duration,
641
+ 'coverage': suite_result.coverage
642
+ },
643
+ 'tests': {
644
+ k: {
645
+ 'success': v.success,
646
+ 'passed': v.passed,
647
+ 'failed': v.failed,
648
+ 'duration': v.duration,
649
+ 'coverage': v.coverage,
650
+ 'not_configured': v.not_configured, # 標記未設定的測試
651
+ 'test_cases': [
652
+ {
653
+ 'name': tc.name,
654
+ 'status': tc.status,
655
+ 'duration': tc.duration,
656
+ 'error': tc.error,
657
+ 'screenshot': tc.screenshot,
658
+ 'api_response': tc.api_response,
659
+ 'terminal_output': tc.terminal_output
660
+ }
661
+ for tc in v.test_cases
662
+ ]
663
+ }
664
+ for k, v in suite_result.results.items()
665
+ }
666
+ }
667
+
668
+ # 額外截圖 (Playwright 測試已自動截圖,這裡只用於補充)
669
+ screenshots = []
670
+ if include_screenshots and screenshot_urls:
671
+ console.print(f"[cyan]擷取補充截圖...[/cyan]")
672
+ screenshots = take_screenshots(project_path, screenshot_urls)
673
+
674
+ # 生成報告
675
+ console.print(f"[cyan]生成 Word 報告...[/cyan]")
676
+ report_path = generate_word_report(
677
+ project_name=project_name,
678
+ test_results=test_results,
679
+ output_path=output_path,
680
+ screenshots=screenshots,
681
+ include_charts=True
682
+ )
683
+
684
+ console.print(f"[green]報告已生成: {report_path}[/green]")
685
+
686
+ return {
687
+ 'success': True,
688
+ 'report_path': report_path,
689
+ 'test_results': test_results
690
+ }