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.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
測試框架初始化工具
|
|
3
|
+
|
|
4
|
+
自動為專案設定測試框架:
|
|
5
|
+
- Vite 專案 → Vitest
|
|
6
|
+
- Angular 專案 → Jest
|
|
7
|
+
- 加入範例測試
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Confirm
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Vitest 設定模板
|
|
21
|
+
VITEST_CONFIG = '''import { defineConfig } from 'vitest/config'
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
test: {
|
|
25
|
+
globals: true,
|
|
26
|
+
environment: 'jsdom',
|
|
27
|
+
coverage: {
|
|
28
|
+
provider: 'v8',
|
|
29
|
+
reporter: ['text', 'html'],
|
|
30
|
+
exclude: ['node_modules/', 'dist/']
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
'''
|
|
35
|
+
|
|
36
|
+
# 範例測試 (Vitest)
|
|
37
|
+
VITEST_EXAMPLE = '''import { describe, it, expect } from 'vitest'
|
|
38
|
+
|
|
39
|
+
describe('範例測試', () => {
|
|
40
|
+
it('應該正確計算加法', () => {
|
|
41
|
+
expect(1 + 1).toBe(2)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('應該正確處理字串', () => {
|
|
45
|
+
const name = '測試'
|
|
46
|
+
expect(name).toContain('測')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
# Jest 設定 (Angular)
|
|
52
|
+
JEST_CONFIG_ANGULAR = '''module.exports = {
|
|
53
|
+
preset: 'jest-preset-angular',
|
|
54
|
+
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
|
55
|
+
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/'],
|
|
56
|
+
coverageDirectory: 'coverage',
|
|
57
|
+
collectCoverageFrom: [
|
|
58
|
+
'src/**/*.ts',
|
|
59
|
+
'!src/**/*.module.ts',
|
|
60
|
+
'!src/main.ts',
|
|
61
|
+
'!src/polyfills.ts'
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
JEST_SETUP_ANGULAR = '''import 'jest-preset-angular/setup-jest';
|
|
67
|
+
'''
|
|
68
|
+
|
|
69
|
+
# Playwright 設定
|
|
70
|
+
PLAYWRIGHT_CONFIG = '''import { defineConfig, devices } from '@playwright/test';
|
|
71
|
+
|
|
72
|
+
export default defineConfig({
|
|
73
|
+
testDir: './e2e',
|
|
74
|
+
timeout: 30000,
|
|
75
|
+
expect: { timeout: 5000 },
|
|
76
|
+
fullyParallel: true,
|
|
77
|
+
retries: process.env.CI ? 2 : 0,
|
|
78
|
+
reporter: 'html',
|
|
79
|
+
use: {
|
|
80
|
+
baseURL: 'http://localhost:5173',
|
|
81
|
+
trace: 'on-first-retry',
|
|
82
|
+
screenshot: 'only-on-failure'
|
|
83
|
+
},
|
|
84
|
+
projects: [
|
|
85
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
86
|
+
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } }
|
|
87
|
+
],
|
|
88
|
+
webServer: {
|
|
89
|
+
command: 'npm run dev',
|
|
90
|
+
url: 'http://localhost:5173',
|
|
91
|
+
reuseExistingServer: !process.env.CI
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
'''
|
|
95
|
+
|
|
96
|
+
PLAYWRIGHT_EXAMPLE = '''import { test, expect } from '@playwright/test';
|
|
97
|
+
|
|
98
|
+
test('首頁應該正確載入', async ({ page }) => {
|
|
99
|
+
await page.goto('/');
|
|
100
|
+
|
|
101
|
+
// 檢查標題
|
|
102
|
+
await expect(page).toHaveTitle(/./);
|
|
103
|
+
|
|
104
|
+
// 檢查主要內容區塊存在
|
|
105
|
+
const main = page.locator('main, #app, .container');
|
|
106
|
+
await expect(main).toBeVisible();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('應該可以導航', async ({ page }) => {
|
|
110
|
+
await page.goto('/');
|
|
111
|
+
|
|
112
|
+
// 點擊連結並確認導航
|
|
113
|
+
// await page.click('text=關於');
|
|
114
|
+
// await expect(page).toHaveURL(/about/);
|
|
115
|
+
});
|
|
116
|
+
'''
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def detect_project_type(project_path: Path) -> dict:
|
|
120
|
+
"""偵測專案類型"""
|
|
121
|
+
package_json = project_path / 'package.json'
|
|
122
|
+
|
|
123
|
+
if not package_json.exists():
|
|
124
|
+
return {'type': 'unknown'}
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
pkg = json.loads(package_json.read_text())
|
|
128
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
129
|
+
|
|
130
|
+
if '@angular/core' in deps:
|
|
131
|
+
return {'type': 'angular', 'pkg': pkg}
|
|
132
|
+
elif 'vite' in deps:
|
|
133
|
+
return {'type': 'vite', 'pkg': pkg}
|
|
134
|
+
elif 'react' in deps:
|
|
135
|
+
return {'type': 'react', 'pkg': pkg}
|
|
136
|
+
else:
|
|
137
|
+
return {'type': 'vanilla', 'pkg': pkg}
|
|
138
|
+
|
|
139
|
+
except Exception:
|
|
140
|
+
return {'type': 'unknown'}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def init_vitest(project_path: Path, pkg: dict) -> dict:
|
|
144
|
+
"""初始化 Vitest"""
|
|
145
|
+
results = {'success': True, 'steps': []}
|
|
146
|
+
|
|
147
|
+
# 1. 安裝依賴
|
|
148
|
+
console.print("[cyan]安裝 Vitest...[/cyan]")
|
|
149
|
+
proc = subprocess.run(
|
|
150
|
+
['npm', 'install', '-D', 'vitest', '@vitest/coverage-v8', 'jsdom'],
|
|
151
|
+
cwd=project_path,
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True
|
|
154
|
+
)
|
|
155
|
+
if proc.returncode != 0:
|
|
156
|
+
results['success'] = False
|
|
157
|
+
results['error'] = proc.stderr
|
|
158
|
+
return results
|
|
159
|
+
results['steps'].append('安裝 vitest, @vitest/coverage-v8, jsdom')
|
|
160
|
+
|
|
161
|
+
# 2. 建立設定檔
|
|
162
|
+
config_path = project_path / 'vitest.config.ts'
|
|
163
|
+
if not config_path.exists():
|
|
164
|
+
config_path.write_text(VITEST_CONFIG)
|
|
165
|
+
results['steps'].append('建立 vitest.config.ts')
|
|
166
|
+
|
|
167
|
+
# 3. 建立測試目錄和範例
|
|
168
|
+
tests_dir = project_path / 'tests'
|
|
169
|
+
tests_dir.mkdir(exist_ok=True)
|
|
170
|
+
|
|
171
|
+
example_path = tests_dir / 'example.test.ts'
|
|
172
|
+
if not example_path.exists():
|
|
173
|
+
example_path.write_text(VITEST_EXAMPLE)
|
|
174
|
+
results['steps'].append('建立 tests/example.test.ts')
|
|
175
|
+
|
|
176
|
+
# 4. 更新 package.json scripts
|
|
177
|
+
package_json = project_path / 'package.json'
|
|
178
|
+
pkg = json.loads(package_json.read_text())
|
|
179
|
+
|
|
180
|
+
if 'scripts' not in pkg:
|
|
181
|
+
pkg['scripts'] = {}
|
|
182
|
+
|
|
183
|
+
pkg['scripts']['test'] = 'vitest'
|
|
184
|
+
pkg['scripts']['test:run'] = 'vitest run'
|
|
185
|
+
pkg['scripts']['test:coverage'] = 'vitest run --coverage'
|
|
186
|
+
|
|
187
|
+
package_json.write_text(json.dumps(pkg, indent=2, ensure_ascii=False))
|
|
188
|
+
results['steps'].append('更新 package.json scripts')
|
|
189
|
+
|
|
190
|
+
return results
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def init_jest_angular(project_path: Path, pkg: dict) -> dict:
|
|
194
|
+
"""初始化 Jest (Angular)"""
|
|
195
|
+
results = {'success': True, 'steps': []}
|
|
196
|
+
|
|
197
|
+
# 1. 安裝依賴
|
|
198
|
+
console.print("[cyan]安裝 Jest for Angular...[/cyan]")
|
|
199
|
+
proc = subprocess.run(
|
|
200
|
+
['npm', 'install', '-D', 'jest', 'jest-preset-angular', '@types/jest'],
|
|
201
|
+
cwd=project_path,
|
|
202
|
+
capture_output=True,
|
|
203
|
+
text=True
|
|
204
|
+
)
|
|
205
|
+
if proc.returncode != 0:
|
|
206
|
+
results['success'] = False
|
|
207
|
+
results['error'] = proc.stderr
|
|
208
|
+
return results
|
|
209
|
+
results['steps'].append('安裝 jest, jest-preset-angular')
|
|
210
|
+
|
|
211
|
+
# 2. 建立設定檔
|
|
212
|
+
config_path = project_path / 'jest.config.js'
|
|
213
|
+
if not config_path.exists():
|
|
214
|
+
config_path.write_text(JEST_CONFIG_ANGULAR)
|
|
215
|
+
results['steps'].append('建立 jest.config.js')
|
|
216
|
+
|
|
217
|
+
setup_path = project_path / 'setup-jest.ts'
|
|
218
|
+
if not setup_path.exists():
|
|
219
|
+
setup_path.write_text(JEST_SETUP_ANGULAR)
|
|
220
|
+
results['steps'].append('建立 setup-jest.ts')
|
|
221
|
+
|
|
222
|
+
# 3. 更新 package.json
|
|
223
|
+
package_json = project_path / 'package.json'
|
|
224
|
+
pkg = json.loads(package_json.read_text())
|
|
225
|
+
|
|
226
|
+
if 'scripts' not in pkg:
|
|
227
|
+
pkg['scripts'] = {}
|
|
228
|
+
|
|
229
|
+
pkg['scripts']['test'] = 'jest'
|
|
230
|
+
pkg['scripts']['test:watch'] = 'jest --watch'
|
|
231
|
+
pkg['scripts']['test:coverage'] = 'jest --coverage'
|
|
232
|
+
|
|
233
|
+
package_json.write_text(json.dumps(pkg, indent=2, ensure_ascii=False))
|
|
234
|
+
results['steps'].append('更新 package.json scripts')
|
|
235
|
+
|
|
236
|
+
return results
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def init_playwright(project_path: Path) -> dict:
|
|
240
|
+
"""初始化 Playwright E2E"""
|
|
241
|
+
results = {'success': True, 'steps': []}
|
|
242
|
+
|
|
243
|
+
# 1. 安裝依賴
|
|
244
|
+
console.print("[cyan]安裝 Playwright...[/cyan]")
|
|
245
|
+
proc = subprocess.run(
|
|
246
|
+
['npm', 'install', '-D', '@playwright/test'],
|
|
247
|
+
cwd=project_path,
|
|
248
|
+
capture_output=True,
|
|
249
|
+
text=True
|
|
250
|
+
)
|
|
251
|
+
if proc.returncode != 0:
|
|
252
|
+
results['success'] = False
|
|
253
|
+
results['error'] = proc.stderr
|
|
254
|
+
return results
|
|
255
|
+
results['steps'].append('安裝 @playwright/test')
|
|
256
|
+
|
|
257
|
+
# 安裝瀏覽器
|
|
258
|
+
console.print("[cyan]安裝瀏覽器...[/cyan]")
|
|
259
|
+
subprocess.run(
|
|
260
|
+
['npx', 'playwright', 'install', 'chromium'],
|
|
261
|
+
cwd=project_path,
|
|
262
|
+
capture_output=True
|
|
263
|
+
)
|
|
264
|
+
results['steps'].append('安裝 Chromium')
|
|
265
|
+
|
|
266
|
+
# 2. 建立設定檔
|
|
267
|
+
config_path = project_path / 'playwright.config.ts'
|
|
268
|
+
if not config_path.exists():
|
|
269
|
+
config_path.write_text(PLAYWRIGHT_CONFIG)
|
|
270
|
+
results['steps'].append('建立 playwright.config.ts')
|
|
271
|
+
|
|
272
|
+
# 3. 建立 e2e 目錄和範例
|
|
273
|
+
e2e_dir = project_path / 'e2e'
|
|
274
|
+
e2e_dir.mkdir(exist_ok=True)
|
|
275
|
+
|
|
276
|
+
example_path = e2e_dir / 'home.spec.ts'
|
|
277
|
+
if not example_path.exists():
|
|
278
|
+
example_path.write_text(PLAYWRIGHT_EXAMPLE)
|
|
279
|
+
results['steps'].append('建立 e2e/home.spec.ts')
|
|
280
|
+
|
|
281
|
+
# 4. 更新 package.json
|
|
282
|
+
package_json = project_path / 'package.json'
|
|
283
|
+
pkg = json.loads(package_json.read_text())
|
|
284
|
+
|
|
285
|
+
pkg['scripts']['e2e'] = 'playwright test'
|
|
286
|
+
pkg['scripts']['e2e:ui'] = 'playwright test --ui'
|
|
287
|
+
pkg['scripts']['e2e:report'] = 'playwright show-report'
|
|
288
|
+
|
|
289
|
+
package_json.write_text(json.dumps(pkg, indent=2, ensure_ascii=False))
|
|
290
|
+
results['steps'].append('更新 package.json scripts')
|
|
291
|
+
|
|
292
|
+
return results
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def run_init_test(project_path: str, include_e2e: bool = False) -> dict:
|
|
296
|
+
"""初始化測試框架"""
|
|
297
|
+
project = Path(project_path)
|
|
298
|
+
project_name = project.name
|
|
299
|
+
|
|
300
|
+
# 偵測專案類型
|
|
301
|
+
info = detect_project_type(project)
|
|
302
|
+
|
|
303
|
+
console.print(Panel(
|
|
304
|
+
f"[bold]{project_name}[/bold] 測試框架初始化",
|
|
305
|
+
border_style="cyan"
|
|
306
|
+
))
|
|
307
|
+
|
|
308
|
+
if info['type'] == 'unknown':
|
|
309
|
+
console.print("[red]無法偵測專案類型 (缺少 package.json)[/red]")
|
|
310
|
+
return {'success': False}
|
|
311
|
+
|
|
312
|
+
console.print(f"[dim]專案類型:[/dim] {info['type']}")
|
|
313
|
+
console.print()
|
|
314
|
+
|
|
315
|
+
results = {'unit': None, 'e2e': None}
|
|
316
|
+
|
|
317
|
+
# 單元測試
|
|
318
|
+
if info['type'] == 'angular':
|
|
319
|
+
console.print("[bold]設定 Jest (Angular)[/bold]")
|
|
320
|
+
results['unit'] = init_jest_angular(project, info.get('pkg', {}))
|
|
321
|
+
else:
|
|
322
|
+
console.print("[bold]設定 Vitest[/bold]")
|
|
323
|
+
results['unit'] = init_vitest(project, info.get('pkg', {}))
|
|
324
|
+
|
|
325
|
+
if results['unit']['success']:
|
|
326
|
+
for step in results['unit']['steps']:
|
|
327
|
+
console.print(f" [green]✓[/green] {step}")
|
|
328
|
+
else:
|
|
329
|
+
console.print(f" [red]✗[/red] {results['unit'].get('error', '未知錯誤')}")
|
|
330
|
+
|
|
331
|
+
# E2E 測試
|
|
332
|
+
if include_e2e:
|
|
333
|
+
console.print()
|
|
334
|
+
console.print("[bold]設定 Playwright (E2E)[/bold]")
|
|
335
|
+
results['e2e'] = init_playwright(project)
|
|
336
|
+
|
|
337
|
+
if results['e2e']['success']:
|
|
338
|
+
for step in results['e2e']['steps']:
|
|
339
|
+
console.print(f" [green]✓[/green] {step}")
|
|
340
|
+
|
|
341
|
+
# 完成訊息
|
|
342
|
+
console.print()
|
|
343
|
+
console.print("[green]測試框架設定完成![/green]")
|
|
344
|
+
console.print()
|
|
345
|
+
console.print("[dim]使用方式:[/dim]")
|
|
346
|
+
console.print(" npm test # 執行測試")
|
|
347
|
+
console.print(" npm run test:coverage # 產生覆蓋率報告")
|
|
348
|
+
if include_e2e:
|
|
349
|
+
console.print(" npm run e2e # 執行 E2E 測試")
|
|
350
|
+
console.print(" npm run e2e:ui # 開啟 Playwright UI")
|
|
351
|
+
|
|
352
|
+
return results
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown 測試報告生成模組
|
|
3
|
+
|
|
4
|
+
生成專業的 Markdown 格式測試報告
|
|
5
|
+
- 測試摘要
|
|
6
|
+
- 測試結果表格
|
|
7
|
+
- 測試案例明細
|
|
8
|
+
- ASCII 圖表
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Dict, List
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_ascii_progress_bar(passed: int, failed: int, width: int = 30) -> str:
|
|
21
|
+
"""建立 ASCII 進度條"""
|
|
22
|
+
total = passed + failed
|
|
23
|
+
if total == 0:
|
|
24
|
+
return "[" + "-" * width + "] N/A"
|
|
25
|
+
|
|
26
|
+
pass_rate = passed / total
|
|
27
|
+
filled = int(width * pass_rate)
|
|
28
|
+
|
|
29
|
+
bar = "[" + "=" * filled + "-" * (width - filled) + "]"
|
|
30
|
+
return f"{bar} {pass_rate * 100:.1f}%"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_duration(duration: float) -> str:
|
|
34
|
+
"""格式化時間"""
|
|
35
|
+
if duration <= 0:
|
|
36
|
+
return "-"
|
|
37
|
+
elif duration < 0.001:
|
|
38
|
+
return f"{duration * 1000000:.0f}us"
|
|
39
|
+
elif duration < 0.1:
|
|
40
|
+
return f"{duration * 1000:.2f}ms"
|
|
41
|
+
elif duration < 1:
|
|
42
|
+
return f"{duration * 1000:.0f}ms"
|
|
43
|
+
else:
|
|
44
|
+
return f"{duration:.2f}s"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def generate_markdown_report(
|
|
48
|
+
project_name: str,
|
|
49
|
+
test_results: Dict,
|
|
50
|
+
output_path: str,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""
|
|
53
|
+
生成 Markdown 測試報告
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
project_name: 專案名稱
|
|
57
|
+
test_results: 測試結果字典 (來自 test_suite)
|
|
58
|
+
output_path: 輸出檔案路徑
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
輸出檔案路徑
|
|
62
|
+
"""
|
|
63
|
+
lines = []
|
|
64
|
+
|
|
65
|
+
# ========== 標題 ==========
|
|
66
|
+
lines.append(f"# {project_name} 測試報告")
|
|
67
|
+
lines.append("")
|
|
68
|
+
|
|
69
|
+
timestamp = test_results.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
|
70
|
+
lines.append(f"> 報告產生時間: {timestamp}")
|
|
71
|
+
lines.append("")
|
|
72
|
+
|
|
73
|
+
# 狀態標示
|
|
74
|
+
overall_success = test_results.get('overall_success', True)
|
|
75
|
+
if overall_success:
|
|
76
|
+
lines.append("")
|
|
77
|
+
else:
|
|
78
|
+
lines.append("")
|
|
79
|
+
lines.append("")
|
|
80
|
+
|
|
81
|
+
# ========== 測試摘要 ==========
|
|
82
|
+
lines.append("## 測試摘要")
|
|
83
|
+
lines.append("")
|
|
84
|
+
|
|
85
|
+
summary = test_results.get('summary', {})
|
|
86
|
+
total_passed = summary.get('total_passed', 0)
|
|
87
|
+
total_failed = summary.get('total_failed', 0)
|
|
88
|
+
total_duration = summary.get('total_duration', 0)
|
|
89
|
+
coverage = summary.get('coverage', 0)
|
|
90
|
+
|
|
91
|
+
# 從 test_cases 計算總時間
|
|
92
|
+
if total_duration == 0:
|
|
93
|
+
tests = test_results.get('tests', {})
|
|
94
|
+
for result in tests.values():
|
|
95
|
+
test_cases = result.get('test_cases', [])
|
|
96
|
+
total_duration += sum(tc.get('duration', 0) for tc in test_cases)
|
|
97
|
+
|
|
98
|
+
# 進度條
|
|
99
|
+
lines.append("```")
|
|
100
|
+
lines.append(create_ascii_progress_bar(total_passed, total_failed, 40))
|
|
101
|
+
lines.append("```")
|
|
102
|
+
lines.append("")
|
|
103
|
+
|
|
104
|
+
# 摘要表格
|
|
105
|
+
lines.append("| 項目 | 數值 |")
|
|
106
|
+
lines.append("|------|------|")
|
|
107
|
+
lines.append(f"| 總測試數 | {total_passed + total_failed} |")
|
|
108
|
+
lines.append(f"| 通過 | {total_passed} |")
|
|
109
|
+
lines.append(f"| 失敗 | {total_failed} |")
|
|
110
|
+
lines.append(f"| 執行時間 | {format_duration(total_duration)} |")
|
|
111
|
+
lines.append(f"| 程式碼覆蓋率 | {coverage:.1f}% |" if coverage > 0 else "| 程式碼覆蓋率 | N/A |")
|
|
112
|
+
lines.append("")
|
|
113
|
+
|
|
114
|
+
# ========== 各類型測試結果 ==========
|
|
115
|
+
lines.append("## 各類型測試結果")
|
|
116
|
+
lines.append("")
|
|
117
|
+
|
|
118
|
+
tests = test_results.get('tests', {})
|
|
119
|
+
configured_tests = {k: v for k, v in tests.items() if not v.get('not_configured', False)}
|
|
120
|
+
|
|
121
|
+
if not configured_tests:
|
|
122
|
+
lines.append("*此專案未設定任何測試框架*")
|
|
123
|
+
lines.append("")
|
|
124
|
+
else:
|
|
125
|
+
type_labels = {
|
|
126
|
+
'UIT': '單元測試 (UIT)',
|
|
127
|
+
'SMOKE': '煙霧測試 (Smoke)',
|
|
128
|
+
'E2E': '端對端測試 (E2E)',
|
|
129
|
+
'UAT': '驗收測試 (UAT)'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.append("| 測試類型 | 狀態 | 通過 | 失敗 | 時間 |")
|
|
133
|
+
lines.append("|----------|------|------|------|------|")
|
|
134
|
+
|
|
135
|
+
for test_type, result in configured_tests.items():
|
|
136
|
+
label = type_labels.get(test_type, test_type)
|
|
137
|
+
success = result.get('success', True)
|
|
138
|
+
status = "PASS" if success else "FAIL"
|
|
139
|
+
passed = result.get('passed', 0)
|
|
140
|
+
failed = result.get('failed', 0)
|
|
141
|
+
|
|
142
|
+
duration = result.get('duration', 0)
|
|
143
|
+
if duration == 0:
|
|
144
|
+
test_cases = result.get('test_cases', [])
|
|
145
|
+
duration = sum(tc.get('duration', 0) for tc in test_cases)
|
|
146
|
+
|
|
147
|
+
lines.append(f"| {label} | {status} | {passed} | {failed} | {format_duration(duration)} |")
|
|
148
|
+
|
|
149
|
+
lines.append("")
|
|
150
|
+
|
|
151
|
+
# ========== 測試案例明細 ==========
|
|
152
|
+
for test_type, result in configured_tests.items():
|
|
153
|
+
test_cases = result.get('test_cases', [])
|
|
154
|
+
if not test_cases:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
type_labels = {
|
|
158
|
+
'UIT': '單元測試 (UIT)',
|
|
159
|
+
'SMOKE': '煙霧測試 (Smoke)',
|
|
160
|
+
'E2E': '端對端測試 (E2E)',
|
|
161
|
+
'UAT': '驗收測試 (UAT)'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lines.append(f"### {type_labels.get(test_type, test_type)} 明細")
|
|
165
|
+
lines.append("")
|
|
166
|
+
|
|
167
|
+
# 分類顯示
|
|
168
|
+
passed_cases = [tc for tc in test_cases if tc.get('status') == 'passed']
|
|
169
|
+
failed_cases = [tc for tc in test_cases if tc.get('status') == 'failed']
|
|
170
|
+
skipped_cases = [tc for tc in test_cases if tc.get('status') == 'skipped']
|
|
171
|
+
|
|
172
|
+
if passed_cases:
|
|
173
|
+
lines.append("<details>")
|
|
174
|
+
lines.append(f"<summary>通過 ({len(passed_cases)})</summary>")
|
|
175
|
+
lines.append("")
|
|
176
|
+
for tc in passed_cases:
|
|
177
|
+
name = tc.get('name', '')
|
|
178
|
+
duration = tc.get('duration', 0)
|
|
179
|
+
lines.append(f"- [x] {name} ({format_duration(duration)})")
|
|
180
|
+
lines.append("")
|
|
181
|
+
lines.append("</details>")
|
|
182
|
+
lines.append("")
|
|
183
|
+
|
|
184
|
+
if failed_cases:
|
|
185
|
+
lines.append(f"**失敗 ({len(failed_cases)})**")
|
|
186
|
+
lines.append("")
|
|
187
|
+
for tc in failed_cases:
|
|
188
|
+
name = tc.get('name', '')
|
|
189
|
+
duration = tc.get('duration', 0)
|
|
190
|
+
error = tc.get('error', '')
|
|
191
|
+
lines.append(f"- [ ] {name} ({format_duration(duration)})")
|
|
192
|
+
if error:
|
|
193
|
+
lines.append(f" - Error: `{error[:100]}`")
|
|
194
|
+
lines.append("")
|
|
195
|
+
|
|
196
|
+
if skipped_cases:
|
|
197
|
+
lines.append(f"**跳過 ({len(skipped_cases)})**")
|
|
198
|
+
lines.append("")
|
|
199
|
+
for tc in skipped_cases:
|
|
200
|
+
name = tc.get('name', '')
|
|
201
|
+
lines.append(f"- [ ] ~{name}~")
|
|
202
|
+
lines.append("")
|
|
203
|
+
|
|
204
|
+
# ========== 測試類型說明 ==========
|
|
205
|
+
lines.append("---")
|
|
206
|
+
lines.append("")
|
|
207
|
+
lines.append("## 測試類型說明")
|
|
208
|
+
lines.append("")
|
|
209
|
+
|
|
210
|
+
descriptions = [
|
|
211
|
+
('UIT', 'Unit Integration Testing', '單元測試驗證各個模組、函數的正確性。使用 Vitest/Jest 框架執行,並產生程式碼覆蓋率報告。'),
|
|
212
|
+
('Smoke', '煙霧測試', '快速驗證系統關鍵路徑是否正常運作。包含應用程式啟動、頁面載入、API 健康檢查等基本功能。'),
|
|
213
|
+
('E2E', 'End-to-End Testing', '端對端測試模擬真實使用情境,驗證完整的使用者流程。使用 Playwright 自動化測試框架執行。'),
|
|
214
|
+
('UAT', 'User Acceptance Testing', '使用者驗收測試從業務角度驗證系統符合需求規格。測試案例依據使用者角色設計,確保系統滿足業務需求。'),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
for abbr, full, desc in descriptions:
|
|
218
|
+
lines.append(f"- **{abbr}** ({full}): {desc}")
|
|
219
|
+
|
|
220
|
+
lines.append("")
|
|
221
|
+
lines.append("---")
|
|
222
|
+
lines.append("")
|
|
223
|
+
lines.append("*Generated by DashAI DevTools*")
|
|
224
|
+
|
|
225
|
+
# 儲存
|
|
226
|
+
content = "\n".join(lines)
|
|
227
|
+
output_file = Path(output_path)
|
|
228
|
+
output_file.write_text(content, encoding='utf-8')
|
|
229
|
+
|
|
230
|
+
return str(output_file)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def run_and_generate_markdown_report(
|
|
234
|
+
project_path: str,
|
|
235
|
+
output_path: str = None,
|
|
236
|
+
test_types: List[str] = None,
|
|
237
|
+
) -> Dict:
|
|
238
|
+
"""
|
|
239
|
+
執行測試並生成 Markdown 報告
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
project_path: 專案路徑
|
|
243
|
+
output_path: 輸出路徑 (預設為專案目錄下的 test-report.md)
|
|
244
|
+
test_types: 測試類型列表
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
結果字典
|
|
248
|
+
"""
|
|
249
|
+
from .test_suite import TestSuiteRunner
|
|
250
|
+
|
|
251
|
+
project = Path(project_path).resolve() # 使用 resolve() 取得完整路徑
|
|
252
|
+
project_name = project.name
|
|
253
|
+
|
|
254
|
+
if not output_path:
|
|
255
|
+
output_path = str(project / f'{project_name}-test-report.md')
|
|
256
|
+
|
|
257
|
+
# 執行測試
|
|
258
|
+
console.print(f"[cyan]執行 {project_name} 測試套件...[/cyan]")
|
|
259
|
+
runner = TestSuiteRunner(project_path)
|
|
260
|
+
suite_result = runner.run_all(test_types)
|
|
261
|
+
|
|
262
|
+
# 準備測試結果
|
|
263
|
+
test_results = {
|
|
264
|
+
'project': project_name,
|
|
265
|
+
'timestamp': suite_result.timestamp,
|
|
266
|
+
'overall_success': suite_result.overall_success,
|
|
267
|
+
'summary': {
|
|
268
|
+
'total_passed': suite_result.total_passed,
|
|
269
|
+
'total_failed': suite_result.total_failed,
|
|
270
|
+
'total_duration': suite_result.total_duration,
|
|
271
|
+
'coverage': suite_result.coverage
|
|
272
|
+
},
|
|
273
|
+
'tests': {
|
|
274
|
+
k: {
|
|
275
|
+
'success': v.success,
|
|
276
|
+
'passed': v.passed,
|
|
277
|
+
'failed': v.failed,
|
|
278
|
+
'duration': v.duration,
|
|
279
|
+
'coverage': v.coverage,
|
|
280
|
+
'not_configured': v.not_configured,
|
|
281
|
+
'test_cases': [
|
|
282
|
+
{
|
|
283
|
+
'name': tc.name,
|
|
284
|
+
'status': tc.status,
|
|
285
|
+
'duration': tc.duration,
|
|
286
|
+
'error': tc.error,
|
|
287
|
+
}
|
|
288
|
+
for tc in v.test_cases
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
for k, v in suite_result.results.items()
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# 生成報告
|
|
296
|
+
console.print(f"[cyan]生成 Markdown 報告...[/cyan]")
|
|
297
|
+
report_path = generate_markdown_report(
|
|
298
|
+
project_name=project_name,
|
|
299
|
+
test_results=test_results,
|
|
300
|
+
output_path=output_path,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
console.print(f"[green]報告已生成: {report_path}[/green]")
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
'success': True,
|
|
307
|
+
'report_path': report_path,
|
|
308
|
+
'test_results': test_results
|
|
309
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
遷移工具集
|
|
3
|
+
|
|
4
|
+
注意:Shoelace → DaisyUI 遷移器已棄用
|
|
5
|
+
目前維持 Shoelace 作為非 Angular 專案的標準 UI 框架
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = ['run_migration']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_migration(project, dry_run=False, from_framework='shoelace', to_framework='daisyui'):
|
|
12
|
+
"""執行遷移
|
|
13
|
+
|
|
14
|
+
注意:此功能已暫停使用
|
|
15
|
+
- Shoelace 維持為標準 UI 框架
|
|
16
|
+
- 遷移需要完整理解設計邏輯後才能進行
|
|
17
|
+
"""
|
|
18
|
+
return {
|
|
19
|
+
'success': False,
|
|
20
|
+
'error': '遷移功能已暫停。UI 框架遷移需要完整理解設計邏輯後手動進行。'
|
|
21
|
+
}
|