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,1241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GAS MES 完整四大測試模組
|
|
3
|
+
|
|
4
|
+
針對 Google Apps Script MES 系統的完整測試套件:
|
|
5
|
+
- UIT: 程式碼靜態分析 (85+ API、14 模組、HTML 品質)
|
|
6
|
+
- Smoke: 全模組頁面載入測試 (桌面版 + 手機版)
|
|
7
|
+
- E2E: 各模組 CRUD 功能驗證
|
|
8
|
+
- UAT: 6 種角色 × 14 模組權限驗證
|
|
9
|
+
|
|
10
|
+
使用方式:
|
|
11
|
+
dash gas-test /path/to/gas/mes
|
|
12
|
+
dash gas-test --word report.docx
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import json
|
|
17
|
+
import subprocess
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Dict, List, Optional, Tuple, Set
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
from rich.panel import Panel
|
|
26
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
# GAS MES 部署 URL
|
|
31
|
+
GAS_MES_URL = "https://script.google.com/macros/s/AKfycbwbX1uACKWhzRhe8JxlXwKEWbZ7ysduAQtf2R2drxIZm5X6acMX7WFUMEpCGouPELoKYw/exec"
|
|
32
|
+
|
|
33
|
+
# ========== 模組與 API 定義 ==========
|
|
34
|
+
|
|
35
|
+
# 14 個功能模組及其對應 API
|
|
36
|
+
MODULE_APIS = {
|
|
37
|
+
'dashboard': {
|
|
38
|
+
'name': '戰情中心',
|
|
39
|
+
'tab_id': 'dashboard',
|
|
40
|
+
'apis': ['getScheduleStats', 'getWorkOrders', 'getDispatches', 'getReports'],
|
|
41
|
+
'required_apis': ['getScheduleStats'],
|
|
42
|
+
},
|
|
43
|
+
'work-orders': {
|
|
44
|
+
'name': '工單管理',
|
|
45
|
+
'tab_id': 'work-orders',
|
|
46
|
+
'apis': ['getWorkOrders', 'createWorkOrder', 'updateWorkOrder', 'deleteWorkOrder', 'cleanInvalidWorkOrders'],
|
|
47
|
+
'required_apis': ['getWorkOrders', 'createWorkOrder', 'updateWorkOrder'],
|
|
48
|
+
'crud': ['create', 'read', 'update', 'delete'],
|
|
49
|
+
},
|
|
50
|
+
'dispatches': {
|
|
51
|
+
'name': '現場派工',
|
|
52
|
+
'tab_id': 'dispatches',
|
|
53
|
+
'apis': ['getDispatches', 'createDispatch', 'updateDispatch', 'deleteDispatch', 'startDispatch', 'completeDispatch'],
|
|
54
|
+
'required_apis': ['getDispatches', 'createDispatch', 'startDispatch', 'completeDispatch'],
|
|
55
|
+
'crud': ['create', 'read', 'update', 'delete'],
|
|
56
|
+
},
|
|
57
|
+
'reports': {
|
|
58
|
+
'name': '報工紀錄',
|
|
59
|
+
'tab_id': 'reports',
|
|
60
|
+
'apis': ['getReports', 'createReport', 'getNgDetailsByReport', 'createNgDetail', 'getNgReasons', 'createNgReason', 'updateNgReason', 'deleteNgReason'],
|
|
61
|
+
'required_apis': ['getReports', 'createReport'],
|
|
62
|
+
'crud': ['create', 'read'],
|
|
63
|
+
},
|
|
64
|
+
'outgassing': {
|
|
65
|
+
'name': '釋氣檢驗',
|
|
66
|
+
'tab_id': 'outgassing',
|
|
67
|
+
'apis': ['getOutgassingTests', 'createOutgassingTest', 'getOutgassingSampleInfo'],
|
|
68
|
+
'required_apis': ['getOutgassingTests', 'createOutgassingTest'],
|
|
69
|
+
'crud': ['create', 'read'],
|
|
70
|
+
'features': ['signature', 'sample_info'],
|
|
71
|
+
},
|
|
72
|
+
'aoi': {
|
|
73
|
+
'name': 'AOI 檢驗',
|
|
74
|
+
'tab_id': 'aoi',
|
|
75
|
+
'apis': ['getAoiInspections', 'createAoiInspection', 'importAoiCsv'],
|
|
76
|
+
'required_apis': ['getAoiInspections', 'createAoiInspection'],
|
|
77
|
+
'crud': ['create', 'read'],
|
|
78
|
+
'features': ['csv_import'],
|
|
79
|
+
},
|
|
80
|
+
'r0': {
|
|
81
|
+
'name': '標籤管理',
|
|
82
|
+
'tab_id': 'r0',
|
|
83
|
+
'apis': ['getR0Labels', 'createR0Label', 'updateR0Label', 'syncR0Labels', 'getEpcHistory', 'createEpcHistory', 'checkEpc'],
|
|
84
|
+
'required_apis': ['getR0Labels', 'createR0Label'],
|
|
85
|
+
'crud': ['create', 'read', 'update'],
|
|
86
|
+
'features': ['epc_check', 'sync'],
|
|
87
|
+
},
|
|
88
|
+
'label-editor': {
|
|
89
|
+
'name': '樣式設計',
|
|
90
|
+
'tab_id': 'label-editor',
|
|
91
|
+
'apis': [], # 前端功能,無後端 API
|
|
92
|
+
'required_apis': [],
|
|
93
|
+
'features': ['canvas_editor'],
|
|
94
|
+
},
|
|
95
|
+
'schedule': {
|
|
96
|
+
'name': '排程管理',
|
|
97
|
+
'tab_id': 'schedule',
|
|
98
|
+
'apis': [
|
|
99
|
+
'getSchedulePlans', 'createSchedulePlan', 'updateSchedulePlan', 'getScheduleStats',
|
|
100
|
+
'getShifts', 'createShift', 'updateShift', 'deleteShift', 'getShiftsByDate', 'getShiftsByDateRange', 'importShifts',
|
|
101
|
+
'getEquipmentSchedules', 'createEquipmentSchedule', 'updateEquipmentSchedule', 'deleteEquipmentSchedule', 'getEquipmentSchedulesByDate'
|
|
102
|
+
],
|
|
103
|
+
'required_apis': ['getSchedulePlans', 'getShifts', 'getEquipmentSchedules'],
|
|
104
|
+
'crud': ['create', 'read', 'update', 'delete'],
|
|
105
|
+
'features': ['calendar', 'drag_drop', 'import'],
|
|
106
|
+
},
|
|
107
|
+
'oven': {
|
|
108
|
+
'name': '烘箱監控',
|
|
109
|
+
'tab_id': 'oven',
|
|
110
|
+
'apis': ['getEquipmentSchedules', 'getEquipmentSchedulesByDate'],
|
|
111
|
+
'required_apis': ['getEquipmentSchedules'],
|
|
112
|
+
'features': ['realtime_status'],
|
|
113
|
+
},
|
|
114
|
+
'wms': {
|
|
115
|
+
'name': '倉儲管理',
|
|
116
|
+
'tab_id': 'wms',
|
|
117
|
+
'apis': [
|
|
118
|
+
'getWmsLocations', 'createWmsLocation', 'updateWmsLocation', 'deleteWmsLocation', 'initWmsLocations',
|
|
119
|
+
'getWmsInventory', 'getWmsLocationSummary', 'getWmsMovements',
|
|
120
|
+
'wmsInbound', 'wmsOutbound', 'wmsTransfer',
|
|
121
|
+
'getWmsStockTakes', 'createWmsStockTake'
|
|
122
|
+
],
|
|
123
|
+
'required_apis': ['getWmsLocations', 'getWmsInventory', 'wmsInbound', 'wmsOutbound', 'wmsTransfer'],
|
|
124
|
+
'crud': ['create', 'read', 'update', 'delete'],
|
|
125
|
+
'features': ['inbound', 'outbound', 'transfer', 'stock_take'],
|
|
126
|
+
},
|
|
127
|
+
'parts': {
|
|
128
|
+
'name': '物料主檔',
|
|
129
|
+
'tab_id': 'parts',
|
|
130
|
+
'apis': ['getParts', 'createPart', 'updatePart', 'deletePart'],
|
|
131
|
+
'required_apis': ['getParts', 'createPart'],
|
|
132
|
+
'crud': ['create', 'read', 'update', 'delete'],
|
|
133
|
+
},
|
|
134
|
+
'audit': {
|
|
135
|
+
'name': '操作紀錄',
|
|
136
|
+
'tab_id': 'audit',
|
|
137
|
+
'apis': ['getAuditLogs', 'createAuditLog'],
|
|
138
|
+
'required_apis': ['getAuditLogs', 'createAuditLog'],
|
|
139
|
+
'features': ['iso27001', 'login_logout'],
|
|
140
|
+
},
|
|
141
|
+
'settings': {
|
|
142
|
+
'name': '設定',
|
|
143
|
+
'tab_id': 'settings',
|
|
144
|
+
'apis': [
|
|
145
|
+
'getOperators', 'createOperator', 'updateOperator', 'deleteOperator',
|
|
146
|
+
'getCustomers', 'createCustomer', 'updateCustomer', 'deleteCustomer',
|
|
147
|
+
'getProducts', 'createProduct', 'updateProduct', 'deleteProduct',
|
|
148
|
+
'syncDatabase', 'clearCache', 'fixColumnOrder', 'resetMes', 'generateTestData'
|
|
149
|
+
],
|
|
150
|
+
'required_apis': ['getOperators', 'getCustomers', 'getProducts', 'syncDatabase'],
|
|
151
|
+
'crud': ['create', 'read', 'update', 'delete'],
|
|
152
|
+
'features': ['operators', 'customers', 'products', 'sync', 'maintenance'],
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# 系統層級 API
|
|
157
|
+
SYSTEM_APIS = ['getVersion', 'getAllData', 'getShortUrl', 'debugSheet', 'fixSignatureData', 'fixStationName']
|
|
158
|
+
|
|
159
|
+
# 角色權限定義
|
|
160
|
+
ROLE_PERMISSIONS = {
|
|
161
|
+
'operator': ['work-orders-view', 'dispatches', 'reports'],
|
|
162
|
+
'warehouse': ['work-orders-view', 'wms', 'parts'],
|
|
163
|
+
'qc': ['work-orders-view', 'outgassing', 'aoi', 'r0'],
|
|
164
|
+
'clerk': ['work-orders-view', 'wms', 'parts', 'audit', 'settings'],
|
|
165
|
+
'supervisor': ['dashboard', 'work-orders-view', 'dispatches', 'reports',
|
|
166
|
+
'outgassing', 'aoi', 'r0', 'schedule', 'oven', 'wms', 'parts', 'audit'],
|
|
167
|
+
'admin': ['dashboard', 'work-orders', 'dispatches', 'reports', 'outgassing',
|
|
168
|
+
'aoi', 'r0', 'label-editor', 'schedule', 'oven', 'wms', 'parts', 'audit', 'settings']
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# 角色中文名稱
|
|
172
|
+
ROLE_NAMES = {
|
|
173
|
+
'operator': '作業員',
|
|
174
|
+
'warehouse': '倉庫',
|
|
175
|
+
'qc': '品管',
|
|
176
|
+
'clerk': '行政',
|
|
177
|
+
'supervisor': '主管',
|
|
178
|
+
'admin': '管理員'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class TestCase:
|
|
184
|
+
"""測試案例"""
|
|
185
|
+
id: str
|
|
186
|
+
name: str
|
|
187
|
+
module: str = ''
|
|
188
|
+
status: str = 'pending' # pending, passed, failed, skipped
|
|
189
|
+
duration: float = 0.0
|
|
190
|
+
error: str = ''
|
|
191
|
+
screenshot: str = ''
|
|
192
|
+
details: str = ''
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class TestTypeResult:
|
|
197
|
+
"""測試類型結果"""
|
|
198
|
+
test_type: str
|
|
199
|
+
passed: int = 0
|
|
200
|
+
failed: int = 0
|
|
201
|
+
skipped: int = 0
|
|
202
|
+
duration: float = 0.0
|
|
203
|
+
success: bool = True
|
|
204
|
+
error: str = ''
|
|
205
|
+
test_cases: List[TestCase] = field(default_factory=list)
|
|
206
|
+
not_configured: bool = False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class GASMESTestResult:
|
|
211
|
+
"""GAS MES 測試結果"""
|
|
212
|
+
project_name: str = 'GAS MES'
|
|
213
|
+
results: Dict[str, TestTypeResult] = field(default_factory=dict)
|
|
214
|
+
total_passed: int = 0
|
|
215
|
+
total_failed: int = 0
|
|
216
|
+
total_duration: float = 0.0
|
|
217
|
+
overall_success: bool = True
|
|
218
|
+
timestamp: str = ''
|
|
219
|
+
version: str = ''
|
|
220
|
+
api_count: int = 0
|
|
221
|
+
module_count: int = 14
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class GASMESTestRunner:
|
|
225
|
+
"""GAS MES 完整測試執行器"""
|
|
226
|
+
|
|
227
|
+
def __init__(self, project_path: str, url: str = None):
|
|
228
|
+
self.project_path = Path(project_path)
|
|
229
|
+
self.url = url or GAS_MES_URL
|
|
230
|
+
self.version = self._get_version()
|
|
231
|
+
self.screenshot_dir = Path(tempfile.mkdtemp(prefix='gas-mes-test-'))
|
|
232
|
+
self.code_content = self._read_file('Code.js')
|
|
233
|
+
self.db_content = self._read_file('Database.js')
|
|
234
|
+
self.app_content = self._read_file('app.html')
|
|
235
|
+
self.discovered_apis = self._discover_apis()
|
|
236
|
+
|
|
237
|
+
def _read_file(self, filename: str) -> str:
|
|
238
|
+
"""讀取檔案內容"""
|
|
239
|
+
filepath = self.project_path / filename
|
|
240
|
+
if filepath.exists():
|
|
241
|
+
return filepath.read_text(encoding='utf-8')
|
|
242
|
+
return ''
|
|
243
|
+
|
|
244
|
+
def _get_version(self) -> str:
|
|
245
|
+
"""從 Code.js 取得版本號"""
|
|
246
|
+
code_js = self.project_path / 'Code.js'
|
|
247
|
+
if code_js.exists():
|
|
248
|
+
content = code_js.read_text(encoding='utf-8')
|
|
249
|
+
match = re.search(r"case\s+['\"]getVersion['\"]:\s*\n?\s*return\s*{\s*success:\s*true,\s*data:\s*['\"]([^'\"]+)['\"]", content)
|
|
250
|
+
if match:
|
|
251
|
+
return match.group(1)
|
|
252
|
+
return 'unknown'
|
|
253
|
+
|
|
254
|
+
def _discover_apis(self) -> Set[str]:
|
|
255
|
+
"""從 Code.js 發現所有 API"""
|
|
256
|
+
apis = set()
|
|
257
|
+
if self.code_content:
|
|
258
|
+
matches = re.findall(r"case\s+['\"](\w+)['\"]:", self.code_content)
|
|
259
|
+
apis = set(matches)
|
|
260
|
+
return apis
|
|
261
|
+
|
|
262
|
+
def _get_puppeteer_cwd(self) -> str:
|
|
263
|
+
"""取得有安裝 Puppeteer 的目錄"""
|
|
264
|
+
check_dirs = [
|
|
265
|
+
'/Users/dash/Documents/github/smai-process-vision',
|
|
266
|
+
'/Users/dash/Documents/github/MES',
|
|
267
|
+
str(self.project_path),
|
|
268
|
+
]
|
|
269
|
+
for check_dir in check_dirs:
|
|
270
|
+
node_modules = Path(check_dir) / 'node_modules' / 'puppeteer'
|
|
271
|
+
if node_modules.exists():
|
|
272
|
+
return check_dir
|
|
273
|
+
return '/Users/dash/Documents/github/smai-process-vision'
|
|
274
|
+
|
|
275
|
+
def _run_puppeteer_test(self, script: str, timeout: int = 60000) -> Dict:
|
|
276
|
+
"""執行 Puppeteer 測試腳本"""
|
|
277
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
|
278
|
+
f.write(script)
|
|
279
|
+
script_path = f.name
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
puppeteer_cwd = self._get_puppeteer_cwd()
|
|
283
|
+
env = {**subprocess.os.environ, 'NODE_PATH': f"{puppeteer_cwd}/node_modules"}
|
|
284
|
+
|
|
285
|
+
proc = subprocess.run(
|
|
286
|
+
['node', script_path],
|
|
287
|
+
capture_output=True,
|
|
288
|
+
text=True,
|
|
289
|
+
timeout=timeout / 1000 + 30,
|
|
290
|
+
cwd=puppeteer_cwd,
|
|
291
|
+
env=env
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if proc.returncode != 0:
|
|
295
|
+
return {'success': False, 'error': proc.stderr[:500]}
|
|
296
|
+
|
|
297
|
+
return json.loads(proc.stdout.strip())
|
|
298
|
+
except subprocess.TimeoutExpired:
|
|
299
|
+
return {'success': False, 'error': 'Timeout'}
|
|
300
|
+
except json.JSONDecodeError as e:
|
|
301
|
+
return {'success': False, 'error': f'Invalid JSON: {str(e)[:100]}'}
|
|
302
|
+
finally:
|
|
303
|
+
Path(script_path).unlink(missing_ok=True)
|
|
304
|
+
|
|
305
|
+
# ========== UIT: 完整靜態分析 ==========
|
|
306
|
+
|
|
307
|
+
def run_uit(self) -> TestTypeResult:
|
|
308
|
+
"""執行 UIT 靜態分析測試 - 涵蓋所有模組"""
|
|
309
|
+
result = TestTypeResult(test_type='UIT')
|
|
310
|
+
start_time = datetime.now()
|
|
311
|
+
|
|
312
|
+
# 1. 系統層級測試
|
|
313
|
+
system_tests = [
|
|
314
|
+
('UIT-SYS-01', '系統', 'Code.js 版本管理', self._check_version_management),
|
|
315
|
+
('UIT-SYS-02', '系統', 'Database.js 資料表定義', self._check_database_schema),
|
|
316
|
+
('UIT-SYS-03', '系統', 'API 回應格式一致性', self._check_api_format),
|
|
317
|
+
('UIT-SYS-04', '系統', 'appsscript.json 設定', self._check_appsscript_config),
|
|
318
|
+
('UIT-SYS-05', '系統', 'HTML v-for :key 綁定', self._check_vfor_key),
|
|
319
|
+
('UIT-SYS-06', '系統', 'HTML 標籤閉合', self._check_html_tags),
|
|
320
|
+
('UIT-SYS-07', '系統', f'API 總數驗證 ({len(self.discovered_apis)} 個)', self._check_api_count),
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
for test_id, module, test_name, test_func in system_tests:
|
|
324
|
+
tc = self._run_test_case(test_id, test_name, module, test_func)
|
|
325
|
+
result.test_cases.append(tc)
|
|
326
|
+
if tc.status == 'passed':
|
|
327
|
+
result.passed += 1
|
|
328
|
+
else:
|
|
329
|
+
result.failed += 1
|
|
330
|
+
|
|
331
|
+
# 2. 各模組 API 測試
|
|
332
|
+
for module_id, module_info in MODULE_APIS.items():
|
|
333
|
+
test_id = f'UIT-{module_id.upper()[:3]}-API'
|
|
334
|
+
test_name = f"{module_info['name']} API 完整性"
|
|
335
|
+
|
|
336
|
+
tc = TestCase(id=test_id, name=test_name, module=module_info['name'])
|
|
337
|
+
tc_start = datetime.now()
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
required = module_info.get('required_apis', [])
|
|
341
|
+
if not required:
|
|
342
|
+
tc.status = 'passed'
|
|
343
|
+
tc.details = '無必要 API (前端功能)'
|
|
344
|
+
else:
|
|
345
|
+
missing = [api for api in required if api not in self.discovered_apis]
|
|
346
|
+
if missing:
|
|
347
|
+
tc.status = 'failed'
|
|
348
|
+
tc.error = f"缺少 API: {', '.join(missing)}"
|
|
349
|
+
else:
|
|
350
|
+
tc.status = 'passed'
|
|
351
|
+
tc.details = f"所有 {len(required)} 個必要 API 存在"
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
tc.status = 'failed'
|
|
355
|
+
tc.error = str(e)
|
|
356
|
+
|
|
357
|
+
tc.duration = (datetime.now() - tc_start).total_seconds()
|
|
358
|
+
result.test_cases.append(tc)
|
|
359
|
+
if tc.status == 'passed':
|
|
360
|
+
result.passed += 1
|
|
361
|
+
else:
|
|
362
|
+
result.failed += 1
|
|
363
|
+
|
|
364
|
+
# 3. 特殊功能測試
|
|
365
|
+
feature_tests = [
|
|
366
|
+
('UIT-FEAT-01', '功能', '角色權限定義 (6 種角色)', self._check_role_permissions),
|
|
367
|
+
('UIT-FEAT-02', '功能', 'ISO 27001 稽核日誌', self._check_audit_logging),
|
|
368
|
+
('UIT-FEAT-03', '功能', 'PIN 碼驗證功能', self._check_pin_verification),
|
|
369
|
+
('UIT-FEAT-04', '功能', '簽名功能 (Canvas)', self._check_signature_feature),
|
|
370
|
+
('UIT-FEAT-05', '功能', 'CSV 匯入功能', self._check_csv_import),
|
|
371
|
+
('UIT-FEAT-06', '功能', '同步功能 (syncDatabase)', self._check_sync_feature),
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
for test_id, module, test_name, test_func in feature_tests:
|
|
375
|
+
tc = self._run_test_case(test_id, test_name, module, test_func)
|
|
376
|
+
result.test_cases.append(tc)
|
|
377
|
+
if tc.status == 'passed':
|
|
378
|
+
result.passed += 1
|
|
379
|
+
else:
|
|
380
|
+
result.failed += 1
|
|
381
|
+
|
|
382
|
+
result.duration = (datetime.now() - start_time).total_seconds()
|
|
383
|
+
result.success = result.failed == 0
|
|
384
|
+
return result
|
|
385
|
+
|
|
386
|
+
def _run_test_case(self, test_id: str, test_name: str, module: str, test_func) -> TestCase:
|
|
387
|
+
"""執行單一測試案例"""
|
|
388
|
+
tc = TestCase(id=test_id, name=test_name, module=module)
|
|
389
|
+
tc_start = datetime.now()
|
|
390
|
+
try:
|
|
391
|
+
passed, details = test_func()
|
|
392
|
+
tc.status = 'passed' if passed else 'failed'
|
|
393
|
+
tc.details = details
|
|
394
|
+
if not passed:
|
|
395
|
+
tc.error = details
|
|
396
|
+
except Exception as e:
|
|
397
|
+
tc.status = 'failed'
|
|
398
|
+
tc.error = str(e)
|
|
399
|
+
tc.duration = (datetime.now() - tc_start).total_seconds()
|
|
400
|
+
return tc
|
|
401
|
+
|
|
402
|
+
def _check_version_management(self) -> Tuple[bool, str]:
|
|
403
|
+
if "case 'getVersion':" in self.code_content or 'case "getVersion":' in self.code_content:
|
|
404
|
+
return True, f'版本號: {self.version}'
|
|
405
|
+
return False, '缺少 getVersion API'
|
|
406
|
+
|
|
407
|
+
def _check_database_schema(self) -> Tuple[bool, str]:
|
|
408
|
+
if not self.db_content:
|
|
409
|
+
return False, 'Database.js 不存在'
|
|
410
|
+
has_config = 'DB_CONFIG' in self.db_content or 'SCHEMA' in self.db_content
|
|
411
|
+
if not has_config:
|
|
412
|
+
return False, '缺少資料庫設定定義'
|
|
413
|
+
tables = re.findall(r'(\w+):\s*{\s*name:\s*[\'"](\w+)[\'"]', self.db_content)
|
|
414
|
+
if tables:
|
|
415
|
+
return True, f'定義 {len(tables)} 個資料表'
|
|
416
|
+
headers_count = len(re.findall(r'headers:\s*\[', self.db_content))
|
|
417
|
+
if headers_count > 0:
|
|
418
|
+
return True, f'定義 {headers_count} 個資料表'
|
|
419
|
+
return False, '無法解析資料表定義'
|
|
420
|
+
|
|
421
|
+
def _check_api_format(self) -> Tuple[bool, str]:
|
|
422
|
+
success_returns = len(re.findall(r'return\s*{\s*success:\s*true', self.code_content))
|
|
423
|
+
error_returns = len(re.findall(r'return\s*{\s*success:\s*false', self.code_content))
|
|
424
|
+
if success_returns > 0 and error_returns > 0:
|
|
425
|
+
return True, f'統一格式: {success_returns} 成功 / {error_returns} 錯誤回應'
|
|
426
|
+
return False, '未使用統一 API 回應格式'
|
|
427
|
+
|
|
428
|
+
def _check_appsscript_config(self) -> Tuple[bool, str]:
|
|
429
|
+
config_file = self.project_path / 'appsscript.json'
|
|
430
|
+
if not config_file.exists():
|
|
431
|
+
return False, 'appsscript.json 不存在'
|
|
432
|
+
try:
|
|
433
|
+
config = json.loads(config_file.read_text(encoding='utf-8'))
|
|
434
|
+
runtime = config.get('runtimeVersion', 'DEPRECATED_ES5')
|
|
435
|
+
return True, f'runtime: {runtime}'
|
|
436
|
+
except json.JSONDecodeError as e:
|
|
437
|
+
return False, f'JSON 格式錯誤: {e}'
|
|
438
|
+
|
|
439
|
+
def _check_vfor_key(self) -> Tuple[bool, str]:
|
|
440
|
+
issues = []
|
|
441
|
+
for html_file in self.project_path.glob('*.html'):
|
|
442
|
+
content = html_file.read_text(encoding='utf-8')
|
|
443
|
+
v_for_without_key = re.findall(r'v-for="[^"]*"(?![^>]*:key)', content)
|
|
444
|
+
if v_for_without_key:
|
|
445
|
+
issues.append(f'{html_file.name}: {len(v_for_without_key)} 處')
|
|
446
|
+
if issues:
|
|
447
|
+
return False, f'缺少 :key: {", ".join(issues[:3])}'
|
|
448
|
+
return True, '所有 v-for 都有 :key'
|
|
449
|
+
|
|
450
|
+
def _check_html_tags(self) -> Tuple[bool, str]:
|
|
451
|
+
issues = []
|
|
452
|
+
tags_to_check = ['div', 'table', 'form']
|
|
453
|
+
for html_file in self.project_path.glob('*.html'):
|
|
454
|
+
content = html_file.read_text(encoding='utf-8')
|
|
455
|
+
for tag in tags_to_check:
|
|
456
|
+
open_count = len(re.findall(rf'<{tag}[^>]*(?<!/)>', content))
|
|
457
|
+
close_count = len(re.findall(rf'</{tag}>', content))
|
|
458
|
+
if open_count > close_count + 3:
|
|
459
|
+
issues.append(f'{html_file.name}: <{tag}>')
|
|
460
|
+
if issues:
|
|
461
|
+
return False, f'標籤可能未閉合: {"; ".join(issues[:3])}'
|
|
462
|
+
return True, 'HTML 標籤正確閉合'
|
|
463
|
+
|
|
464
|
+
def _check_api_count(self) -> Tuple[bool, str]:
|
|
465
|
+
count = len(self.discovered_apis)
|
|
466
|
+
if count >= 80:
|
|
467
|
+
return True, f'發現 {count} 個 API (預期 85+)'
|
|
468
|
+
return False, f'API 數量不足: {count} (預期 85+)'
|
|
469
|
+
|
|
470
|
+
def _check_role_permissions(self) -> Tuple[bool, str]:
|
|
471
|
+
roles = ['operator', 'warehouse', 'qc', 'clerk', 'supervisor', 'admin']
|
|
472
|
+
found_roles = [r for r in roles if f'{r}:' in self.app_content]
|
|
473
|
+
if len(found_roles) >= 6:
|
|
474
|
+
return True, f'定義 {len(found_roles)} 種角色'
|
|
475
|
+
return False, f'角色定義不完整: {found_roles}'
|
|
476
|
+
|
|
477
|
+
def _check_audit_logging(self) -> Tuple[bool, str]:
|
|
478
|
+
has_log = 'logAudit' in self.app_content or 'createAuditLog' in self.code_content
|
|
479
|
+
has_login = "'login'" in self.app_content or '"login"' in self.app_content
|
|
480
|
+
has_logout = "'logout'" in self.app_content or '"logout"' in self.app_content
|
|
481
|
+
if has_log and has_login and has_logout:
|
|
482
|
+
return True, 'ISO 27001: 登入/登出/操作記錄完整'
|
|
483
|
+
if has_log:
|
|
484
|
+
return True, '有稽核日誌功能 (建議加入登入/登出記錄)'
|
|
485
|
+
return False, '缺少稽核日誌功能'
|
|
486
|
+
|
|
487
|
+
def _check_pin_verification(self) -> Tuple[bool, str]:
|
|
488
|
+
has_pin = 'verifyPin' in self.app_content and 'showPinModal' in self.app_content
|
|
489
|
+
if has_pin:
|
|
490
|
+
return True, 'PIN 碼驗證功能已實作'
|
|
491
|
+
return False, '缺少 PIN 碼驗證功能'
|
|
492
|
+
|
|
493
|
+
def _check_signature_feature(self) -> Tuple[bool, str]:
|
|
494
|
+
has_signature = 'canvas' in self.app_content.lower() and 'signature' in self.app_content.lower()
|
|
495
|
+
if has_signature:
|
|
496
|
+
return True, '簽名功能 (Canvas) 已實作'
|
|
497
|
+
return False, '缺少簽名功能'
|
|
498
|
+
|
|
499
|
+
def _check_csv_import(self) -> Tuple[bool, str]:
|
|
500
|
+
has_import = 'importAoiCsv' in self.discovered_apis or 'importShifts' in self.discovered_apis
|
|
501
|
+
if has_import:
|
|
502
|
+
return True, 'CSV 匯入功能已實作'
|
|
503
|
+
return False, '缺少 CSV 匯入功能'
|
|
504
|
+
|
|
505
|
+
def _check_sync_feature(self) -> Tuple[bool, str]:
|
|
506
|
+
if 'syncDatabase' in self.discovered_apis:
|
|
507
|
+
return True, '同步功能 (syncDatabase) 已實作'
|
|
508
|
+
return False, '缺少同步功能'
|
|
509
|
+
|
|
510
|
+
# ========== Smoke: 全模組頁面載入測試 ==========
|
|
511
|
+
|
|
512
|
+
def run_smoke(self) -> TestTypeResult:
|
|
513
|
+
"""執行 Smoke 煙霧測試 - 涵蓋所有模組"""
|
|
514
|
+
result = TestTypeResult(test_type='Smoke')
|
|
515
|
+
start_time = datetime.now()
|
|
516
|
+
|
|
517
|
+
# 1. 基礎載入測試
|
|
518
|
+
basic_tests = [
|
|
519
|
+
('SMOKE-01', '系統', '應用程式載入', 'load'),
|
|
520
|
+
('SMOKE-02', '系統', '無 JavaScript 錯誤', 'errors'),
|
|
521
|
+
('SMOKE-03', '系統', '手機版無水平溢出', 'mobile'),
|
|
522
|
+
('SMOKE-04', '系統', '版本號驗證', 'version'),
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
for test_id, module, test_name, test_type in basic_tests:
|
|
526
|
+
tc = TestCase(id=test_id, name=test_name, module=module)
|
|
527
|
+
tc_start = datetime.now()
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
if test_type == 'load':
|
|
531
|
+
passed, details, screenshot = self._test_page_load()
|
|
532
|
+
elif test_type == 'errors':
|
|
533
|
+
passed, details, screenshot = self._test_no_js_errors()
|
|
534
|
+
elif test_type == 'mobile':
|
|
535
|
+
passed, details, screenshot = self._test_mobile_overflow()
|
|
536
|
+
elif test_type == 'version':
|
|
537
|
+
passed, details, screenshot = self._test_version_display()
|
|
538
|
+
else:
|
|
539
|
+
passed, details, screenshot = False, '未知測試', ''
|
|
540
|
+
|
|
541
|
+
tc.status = 'passed' if passed else 'failed'
|
|
542
|
+
tc.details = details
|
|
543
|
+
tc.screenshot = screenshot
|
|
544
|
+
if not passed:
|
|
545
|
+
tc.error = details
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
tc.status = 'failed'
|
|
549
|
+
tc.error = str(e)
|
|
550
|
+
|
|
551
|
+
tc.duration = (datetime.now() - tc_start).total_seconds()
|
|
552
|
+
result.test_cases.append(tc)
|
|
553
|
+
if tc.status == 'passed':
|
|
554
|
+
result.passed += 1
|
|
555
|
+
else:
|
|
556
|
+
result.failed += 1
|
|
557
|
+
|
|
558
|
+
# 2. 各模組頁籤截圖測試
|
|
559
|
+
module_screenshots = self._test_all_module_screenshots()
|
|
560
|
+
module_names = {
|
|
561
|
+
'dashboard': '戰情中心', 'work-orders': '工單管理', 'dispatches': '現場派工',
|
|
562
|
+
'reports': '報工紀錄', 'outgassing': '釋氣檢驗', 'aoi': 'AOI檢驗',
|
|
563
|
+
'r0': '標籤管理', 'label-editor': '樣式設計', 'schedule': '排程管理',
|
|
564
|
+
'oven': '烘箱監控', 'wms': '倉儲管理', 'parts': '物料主檔',
|
|
565
|
+
'audit': '操作紀錄', 'settings': '設定',
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
for tab_id, tab_name in module_names.items():
|
|
569
|
+
test_id = f'SMOKE-TAB-{tab_id.upper()[:3]}'
|
|
570
|
+
tc = TestCase(id=test_id, name=f'{tab_name} 頁籤截圖', module=tab_name)
|
|
571
|
+
|
|
572
|
+
if tab_id in module_screenshots:
|
|
573
|
+
tc.status = 'passed'
|
|
574
|
+
tc.details = f'截圖成功'
|
|
575
|
+
tc.screenshot = module_screenshots[tab_id]
|
|
576
|
+
result.passed += 1
|
|
577
|
+
else:
|
|
578
|
+
tc.status = 'failed'
|
|
579
|
+
tc.error = '截圖失敗或頁籤不存在'
|
|
580
|
+
result.failed += 1
|
|
581
|
+
|
|
582
|
+
result.test_cases.append(tc)
|
|
583
|
+
|
|
584
|
+
# 3. 各模組 HTML 檔案存在性測試
|
|
585
|
+
module_files = {
|
|
586
|
+
'work-orders': 'tab-work-orders.html',
|
|
587
|
+
'dispatches': 'tab-dispatches.html',
|
|
588
|
+
'reports': 'tab-reports.html',
|
|
589
|
+
'outgassing': 'tab-outgassing.html',
|
|
590
|
+
'aoi': 'tab-aoi.html',
|
|
591
|
+
'r0': 'tab-r0.html',
|
|
592
|
+
'label-editor': 'tab-label-editor.html',
|
|
593
|
+
'schedule': 'tab-schedule.html',
|
|
594
|
+
'oven': 'tab-oven.html',
|
|
595
|
+
'wms': 'tab-wms.html',
|
|
596
|
+
'parts': 'tab-parts.html',
|
|
597
|
+
'audit': 'tab-audit.html',
|
|
598
|
+
'settings': 'tab-settings.html',
|
|
599
|
+
'dashboard': 'tab-dashboard.html',
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for module_id, filename in module_files.items():
|
|
603
|
+
module_name = MODULE_APIS.get(module_id, {}).get('name', module_id)
|
|
604
|
+
test_id = f'SMOKE-{module_id.upper()[:3]}'
|
|
605
|
+
tc = TestCase(id=test_id, name=f'{module_name} 頁面檔案', module=module_name)
|
|
606
|
+
|
|
607
|
+
filepath = self.project_path / filename
|
|
608
|
+
if filepath.exists():
|
|
609
|
+
content = filepath.read_text(encoding='utf-8')
|
|
610
|
+
# 檢查是否有實際內容
|
|
611
|
+
if len(content) > 100 and 'v-if' in content:
|
|
612
|
+
tc.status = 'passed'
|
|
613
|
+
tc.details = f'{filename} 存在 ({len(content)} bytes)'
|
|
614
|
+
else:
|
|
615
|
+
tc.status = 'passed'
|
|
616
|
+
tc.details = f'{filename} 存在 (簡易頁面)'
|
|
617
|
+
else:
|
|
618
|
+
tc.status = 'failed'
|
|
619
|
+
tc.error = f'{filename} 不存在'
|
|
620
|
+
|
|
621
|
+
result.test_cases.append(tc)
|
|
622
|
+
if tc.status == 'passed':
|
|
623
|
+
result.passed += 1
|
|
624
|
+
else:
|
|
625
|
+
result.failed += 1
|
|
626
|
+
|
|
627
|
+
result.duration = (datetime.now() - start_time).total_seconds()
|
|
628
|
+
result.success = result.failed == 0
|
|
629
|
+
return result
|
|
630
|
+
|
|
631
|
+
def _test_page_load(self) -> Tuple[bool, str, str]:
|
|
632
|
+
screenshot_path = str(self.screenshot_dir / 'smoke-01-load.png')
|
|
633
|
+
script = f'''
|
|
634
|
+
const puppeteer = require("puppeteer");
|
|
635
|
+
(async () => {{
|
|
636
|
+
const browser = await puppeteer.launch({{ headless: "new" }});
|
|
637
|
+
const page = await browser.newPage();
|
|
638
|
+
await page.setViewport({{ width: 1920, height: 1080 }});
|
|
639
|
+
const result = {{ success: false, loadTime: 0, status: 0 }};
|
|
640
|
+
try {{
|
|
641
|
+
const start = Date.now();
|
|
642
|
+
const response = await page.goto("{self.url}", {{ waitUntil: "networkidle0", timeout: 60000 }});
|
|
643
|
+
result.loadTime = Date.now() - start;
|
|
644
|
+
result.status = response ? response.status() : 0;
|
|
645
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
646
|
+
await page.screenshot({{ path: "{screenshot_path}", fullPage: false }});
|
|
647
|
+
result.success = result.status === 200;
|
|
648
|
+
}} catch (e) {{ result.error = e.message; }}
|
|
649
|
+
await browser.close();
|
|
650
|
+
console.log(JSON.stringify(result));
|
|
651
|
+
}})();
|
|
652
|
+
'''
|
|
653
|
+
result = self._run_puppeteer_test(script)
|
|
654
|
+
if result.get('success'):
|
|
655
|
+
return True, f"載入時間: {result.get('loadTime', 0)}ms", screenshot_path
|
|
656
|
+
return False, result.get('error', '載入失敗'), screenshot_path
|
|
657
|
+
|
|
658
|
+
def _test_no_js_errors(self) -> Tuple[bool, str, str]:
|
|
659
|
+
screenshot_path = str(self.screenshot_dir / 'smoke-02-errors.png')
|
|
660
|
+
script = f'''
|
|
661
|
+
const puppeteer = require("puppeteer");
|
|
662
|
+
(async () => {{
|
|
663
|
+
const browser = await puppeteer.launch({{ headless: "new" }});
|
|
664
|
+
const page = await browser.newPage();
|
|
665
|
+
await page.setViewport({{ width: 1920, height: 1080 }});
|
|
666
|
+
const errors = [];
|
|
667
|
+
page.on("console", msg => {{
|
|
668
|
+
if (msg.type() === "error" && !msg.text().includes("favicon")) {{
|
|
669
|
+
errors.push(msg.text().substring(0, 150));
|
|
670
|
+
}}
|
|
671
|
+
}});
|
|
672
|
+
page.on("pageerror", err => errors.push(err.toString().substring(0, 150)));
|
|
673
|
+
try {{
|
|
674
|
+
await page.goto("{self.url}", {{ waitUntil: "networkidle0", timeout: 60000 }});
|
|
675
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
676
|
+
await page.screenshot({{ path: "{screenshot_path}" }});
|
|
677
|
+
}} catch (e) {{ errors.push(e.message); }}
|
|
678
|
+
await browser.close();
|
|
679
|
+
console.log(JSON.stringify({{ success: errors.length === 0, errors }}));
|
|
680
|
+
}})();
|
|
681
|
+
'''
|
|
682
|
+
result = self._run_puppeteer_test(script)
|
|
683
|
+
if result.get('success'):
|
|
684
|
+
return True, '無 JavaScript 錯誤', screenshot_path
|
|
685
|
+
errors = result.get('errors', [])
|
|
686
|
+
return False, f"發現 {len(errors)} 個錯誤", screenshot_path
|
|
687
|
+
|
|
688
|
+
def _test_mobile_overflow(self) -> Tuple[bool, str, str]:
|
|
689
|
+
screenshot_path = str(self.screenshot_dir / 'smoke-03-mobile.png')
|
|
690
|
+
script = f'''
|
|
691
|
+
const puppeteer = require("puppeteer");
|
|
692
|
+
(async () => {{
|
|
693
|
+
const browser = await puppeteer.launch({{ headless: "new" }});
|
|
694
|
+
const page = await browser.newPage();
|
|
695
|
+
await page.setViewport({{ width: 375, height: 812, isMobile: true }});
|
|
696
|
+
try {{
|
|
697
|
+
await page.goto("{self.url}", {{ waitUntil: "networkidle0", timeout: 60000 }});
|
|
698
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
699
|
+
const hasOverflow = await page.evaluate(() =>
|
|
700
|
+
document.documentElement.scrollWidth > document.documentElement.clientWidth
|
|
701
|
+
);
|
|
702
|
+
await page.screenshot({{ path: "{screenshot_path}", fullPage: true }});
|
|
703
|
+
console.log(JSON.stringify({{ success: !hasOverflow, hasOverflow }}));
|
|
704
|
+
}} catch (e) {{
|
|
705
|
+
console.log(JSON.stringify({{ success: false, error: e.message }}));
|
|
706
|
+
}}
|
|
707
|
+
await browser.close();
|
|
708
|
+
}})();
|
|
709
|
+
'''
|
|
710
|
+
result = self._run_puppeteer_test(script)
|
|
711
|
+
if result.get('success'):
|
|
712
|
+
return True, '手機版無水平溢出', screenshot_path
|
|
713
|
+
if result.get('hasOverflow'):
|
|
714
|
+
return False, '偵測到水平溢出', screenshot_path
|
|
715
|
+
return False, result.get('error', '測試失敗'), screenshot_path
|
|
716
|
+
|
|
717
|
+
def _test_version_display(self) -> Tuple[bool, str, str]:
|
|
718
|
+
screenshot_path = str(self.screenshot_dir / 'smoke-04-version.png')
|
|
719
|
+
if self.version and self.version != 'unknown':
|
|
720
|
+
script = f'''
|
|
721
|
+
const puppeteer = require("puppeteer");
|
|
722
|
+
(async () => {{
|
|
723
|
+
const browser = await puppeteer.launch({{ headless: "new" }});
|
|
724
|
+
const page = await browser.newPage();
|
|
725
|
+
await page.setViewport({{ width: 1920, height: 1080 }});
|
|
726
|
+
try {{
|
|
727
|
+
await page.goto("{self.url}", {{ waitUntil: "networkidle0", timeout: 60000 }});
|
|
728
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
729
|
+
await page.screenshot({{ path: "{screenshot_path}" }});
|
|
730
|
+
console.log(JSON.stringify({{ success: true }}));
|
|
731
|
+
}} catch (e) {{
|
|
732
|
+
console.log(JSON.stringify({{ success: false, error: e.message }}));
|
|
733
|
+
}}
|
|
734
|
+
await browser.close();
|
|
735
|
+
}})();
|
|
736
|
+
'''
|
|
737
|
+
self._run_puppeteer_test(script)
|
|
738
|
+
return True, f'版本號: {self.version}', screenshot_path
|
|
739
|
+
return False, 'Code.js 缺少 getVersion API', screenshot_path
|
|
740
|
+
|
|
741
|
+
def _test_all_module_screenshots(self) -> Dict[str, str]:
|
|
742
|
+
"""對所有模組頁籤進行截圖"""
|
|
743
|
+
tabs_to_capture = [
|
|
744
|
+
('dashboard', '戰情中心'),
|
|
745
|
+
('work-orders', '工單管理'),
|
|
746
|
+
('dispatches', '現場派工'),
|
|
747
|
+
('reports', '報工紀錄'),
|
|
748
|
+
('outgassing', '釋氣檢驗'),
|
|
749
|
+
('aoi', 'AOI檢驗'),
|
|
750
|
+
('r0', '標籤管理'),
|
|
751
|
+
('label-editor', '樣式設計'),
|
|
752
|
+
('schedule', '排程管理'),
|
|
753
|
+
('oven', '烘箱監控'),
|
|
754
|
+
('wms', '倉儲管理'),
|
|
755
|
+
('parts', '物料主檔'),
|
|
756
|
+
('audit', '操作紀錄'),
|
|
757
|
+
('settings', '設定'),
|
|
758
|
+
]
|
|
759
|
+
|
|
760
|
+
screenshots = {}
|
|
761
|
+
screenshot_list = ','.join([f'"{t[0]}"' for t in tabs_to_capture])
|
|
762
|
+
screenshot_paths = {t[0]: str(self.screenshot_dir / f'tab-{t[0]}.png') for t in tabs_to_capture}
|
|
763
|
+
paths_json = ','.join([f'"{t[0]}": "{screenshot_paths[t[0]]}"' for t in tabs_to_capture])
|
|
764
|
+
|
|
765
|
+
script = f'''
|
|
766
|
+
const puppeteer = require("puppeteer");
|
|
767
|
+
(async () => {{
|
|
768
|
+
const browser = await puppeteer.launch({{ headless: "new" }});
|
|
769
|
+
const page = await browser.newPage();
|
|
770
|
+
await page.setViewport({{ width: 1920, height: 1080 }});
|
|
771
|
+
const tabs = [{screenshot_list}];
|
|
772
|
+
const paths = {{{paths_json}}};
|
|
773
|
+
const results = {{}};
|
|
774
|
+
|
|
775
|
+
try {{
|
|
776
|
+
await page.goto("{self.url}", {{ waitUntil: "networkidle0", timeout: 60000 }});
|
|
777
|
+
await new Promise(r => setTimeout(r, 12000));
|
|
778
|
+
|
|
779
|
+
// GAS Web App 結構: frame[0]=main, frame[1]=sandbox, frame[2]=userHtmlFrame (Vue app)
|
|
780
|
+
const frames = page.frames();
|
|
781
|
+
const appFrame = frames[2]; // Vue app is in the innermost frame
|
|
782
|
+
|
|
783
|
+
if (!appFrame) {{
|
|
784
|
+
results.error = "Cannot find app frame";
|
|
785
|
+
console.log(JSON.stringify(results));
|
|
786
|
+
await browser.close();
|
|
787
|
+
return;
|
|
788
|
+
}}
|
|
789
|
+
|
|
790
|
+
// 驗證 Vue app 存在
|
|
791
|
+
const hasVue = await appFrame.evaluate(() => {{
|
|
792
|
+
const appEl = document.getElementById("app");
|
|
793
|
+
return !!(appEl && appEl.__vue_app__);
|
|
794
|
+
}});
|
|
795
|
+
|
|
796
|
+
if (!hasVue) {{
|
|
797
|
+
results.error = "Vue app not found";
|
|
798
|
+
console.log(JSON.stringify(results));
|
|
799
|
+
await browser.close();
|
|
800
|
+
return;
|
|
801
|
+
}}
|
|
802
|
+
|
|
803
|
+
results.foundVue = true;
|
|
804
|
+
|
|
805
|
+
for (const tab of tabs) {{
|
|
806
|
+
try {{
|
|
807
|
+
// 使用 Vue 3 正確的 proxy 存取方式切換頁籤
|
|
808
|
+
const switched = await appFrame.evaluate((t) => {{
|
|
809
|
+
const appEl = document.getElementById("app");
|
|
810
|
+
if (appEl && appEl.__vue_app__) {{
|
|
811
|
+
const comp = appEl.__vue_app__._container?._vnode?.component;
|
|
812
|
+
if (comp && comp.proxy) {{
|
|
813
|
+
comp.proxy.currentTab = t;
|
|
814
|
+
return comp.proxy.currentTab === t;
|
|
815
|
+
}}
|
|
816
|
+
}}
|
|
817
|
+
return false;
|
|
818
|
+
}}, tab);
|
|
819
|
+
|
|
820
|
+
if (!switched) {{
|
|
821
|
+
results[tab] = "switch failed";
|
|
822
|
+
continue;
|
|
823
|
+
}}
|
|
824
|
+
|
|
825
|
+
// 等待 Vue 更新 DOM
|
|
826
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
827
|
+
|
|
828
|
+
// 截圖
|
|
829
|
+
await page.screenshot({{ path: paths[tab], fullPage: false }});
|
|
830
|
+
results[tab] = "ok";
|
|
831
|
+
}} catch (e) {{
|
|
832
|
+
results[tab] = e.message;
|
|
833
|
+
}}
|
|
834
|
+
}}
|
|
835
|
+
}} catch (e) {{
|
|
836
|
+
results.error = e.message;
|
|
837
|
+
}}
|
|
838
|
+
|
|
839
|
+
await browser.close();
|
|
840
|
+
console.log(JSON.stringify(results));
|
|
841
|
+
}})();
|
|
842
|
+
'''
|
|
843
|
+
result = self._run_puppeteer_test(script)
|
|
844
|
+
|
|
845
|
+
for tab_id, tab_name in tabs_to_capture:
|
|
846
|
+
if result.get(tab_id) == 'ok' and Path(screenshot_paths[tab_id]).exists():
|
|
847
|
+
screenshots[tab_id] = screenshot_paths[tab_id]
|
|
848
|
+
|
|
849
|
+
return screenshots
|
|
850
|
+
|
|
851
|
+
# ========== E2E: 各模組 CRUD 功能測試 ==========
|
|
852
|
+
|
|
853
|
+
def run_e2e(self) -> TestTypeResult:
|
|
854
|
+
"""執行 E2E 端對端測試 - 各模組 CRUD 驗證"""
|
|
855
|
+
result = TestTypeResult(test_type='E2E')
|
|
856
|
+
start_time = datetime.now()
|
|
857
|
+
|
|
858
|
+
# 測試每個模組的 CRUD API 是否存在於 Code.js
|
|
859
|
+
for module_id, module_info in MODULE_APIS.items():
|
|
860
|
+
module_name = module_info['name']
|
|
861
|
+
crud_ops = module_info.get('crud', [])
|
|
862
|
+
|
|
863
|
+
if not crud_ops:
|
|
864
|
+
continue # 跳過無 CRUD 的模組
|
|
865
|
+
|
|
866
|
+
for op in crud_ops:
|
|
867
|
+
test_id = f'E2E-{module_id.upper()[:3]}-{op.upper()[:1]}'
|
|
868
|
+
test_name = f'{module_name} - {op.upper()}'
|
|
869
|
+
|
|
870
|
+
tc = TestCase(id=test_id, name=test_name, module=module_name)
|
|
871
|
+
|
|
872
|
+
# 根據操作類型找對應 API
|
|
873
|
+
api_patterns = {
|
|
874
|
+
'create': [f'create{module_id.title().replace("-", "")}', f'create'],
|
|
875
|
+
'read': [f'get{module_id.title().replace("-", "")}', f'get'],
|
|
876
|
+
'update': [f'update{module_id.title().replace("-", "")}', f'update'],
|
|
877
|
+
'delete': [f'delete{module_id.title().replace("-", "")}', f'delete'],
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
# 檢查是否有對應 API
|
|
881
|
+
found_api = None
|
|
882
|
+
for api in module_info.get('apis', []):
|
|
883
|
+
api_lower = api.lower()
|
|
884
|
+
if op == 'create' and api_lower.startswith('create'):
|
|
885
|
+
found_api = api
|
|
886
|
+
break
|
|
887
|
+
elif op == 'read' and api_lower.startswith('get'):
|
|
888
|
+
found_api = api
|
|
889
|
+
break
|
|
890
|
+
elif op == 'update' and api_lower.startswith('update'):
|
|
891
|
+
found_api = api
|
|
892
|
+
break
|
|
893
|
+
elif op == 'delete' and api_lower.startswith('delete'):
|
|
894
|
+
found_api = api
|
|
895
|
+
break
|
|
896
|
+
|
|
897
|
+
if found_api and found_api in self.discovered_apis:
|
|
898
|
+
tc.status = 'passed'
|
|
899
|
+
tc.details = f'API: {found_api}'
|
|
900
|
+
elif found_api:
|
|
901
|
+
tc.status = 'failed'
|
|
902
|
+
tc.error = f'API {found_api} 未實作'
|
|
903
|
+
else:
|
|
904
|
+
tc.status = 'skipped'
|
|
905
|
+
tc.details = f'無 {op} 操作'
|
|
906
|
+
result.skipped += 1
|
|
907
|
+
result.test_cases.append(tc)
|
|
908
|
+
continue
|
|
909
|
+
|
|
910
|
+
result.test_cases.append(tc)
|
|
911
|
+
if tc.status == 'passed':
|
|
912
|
+
result.passed += 1
|
|
913
|
+
else:
|
|
914
|
+
result.failed += 1
|
|
915
|
+
|
|
916
|
+
# 測試特殊功能 API
|
|
917
|
+
special_features = [
|
|
918
|
+
('E2E-WMS-IN', '倉儲管理', 'WMS 入庫', 'wmsInbound'),
|
|
919
|
+
('E2E-WMS-OUT', '倉儲管理', 'WMS 出庫', 'wmsOutbound'),
|
|
920
|
+
('E2E-WMS-TRF', '倉儲管理', 'WMS 移轉', 'wmsTransfer'),
|
|
921
|
+
('E2E-DIS-START', '現場派工', '開始派工', 'startDispatch'),
|
|
922
|
+
('E2E-DIS-COMP', '現場派工', '完成派工', 'completeDispatch'),
|
|
923
|
+
('E2E-AOI-CSV', 'AOI 檢驗', 'CSV 匯入', 'importAoiCsv'),
|
|
924
|
+
('E2E-SCH-IMP', '排程管理', '班表匯入', 'importShifts'),
|
|
925
|
+
('E2E-R0-SYNC', '標籤管理', 'EPC 同步', 'syncR0Labels'),
|
|
926
|
+
('E2E-R0-CHK', '標籤管理', 'EPC 檢查', 'checkEpc'),
|
|
927
|
+
('E2E-SYS-SYNC', '系統', '資料庫同步', 'syncDatabase'),
|
|
928
|
+
]
|
|
929
|
+
|
|
930
|
+
for test_id, module, test_name, api_name in special_features:
|
|
931
|
+
tc = TestCase(id=test_id, name=test_name, module=module)
|
|
932
|
+
if api_name in self.discovered_apis:
|
|
933
|
+
tc.status = 'passed'
|
|
934
|
+
tc.details = f'API: {api_name}'
|
|
935
|
+
result.passed += 1
|
|
936
|
+
else:
|
|
937
|
+
tc.status = 'failed'
|
|
938
|
+
tc.error = f'API {api_name} 不存在'
|
|
939
|
+
result.failed += 1
|
|
940
|
+
result.test_cases.append(tc)
|
|
941
|
+
|
|
942
|
+
result.duration = (datetime.now() - start_time).total_seconds()
|
|
943
|
+
result.success = result.failed == 0
|
|
944
|
+
return result
|
|
945
|
+
|
|
946
|
+
# ========== UAT: 角色權限驗證 ==========
|
|
947
|
+
|
|
948
|
+
def run_uat(self) -> TestTypeResult:
|
|
949
|
+
"""執行 UAT 使用者驗收測試 - 6 角色 × 14 模組"""
|
|
950
|
+
result = TestTypeResult(test_type='UAT')
|
|
951
|
+
start_time = datetime.now()
|
|
952
|
+
|
|
953
|
+
# 1. 角色權限定義測試
|
|
954
|
+
for role, permissions in ROLE_PERMISSIONS.items():
|
|
955
|
+
role_name = ROLE_NAMES.get(role, role)
|
|
956
|
+
test_id = f'UAT-ROLE-{role.upper()[:3]}'
|
|
957
|
+
tc = TestCase(id=test_id, name=f'角色權限: {role_name}', module='權限')
|
|
958
|
+
|
|
959
|
+
# 檢查 app.html 中是否有該角色定義
|
|
960
|
+
if f'{role}:' in self.app_content:
|
|
961
|
+
tc.status = 'passed'
|
|
962
|
+
tc.details = f'{len(permissions)} 個頁籤權限'
|
|
963
|
+
result.passed += 1
|
|
964
|
+
else:
|
|
965
|
+
tc.status = 'failed'
|
|
966
|
+
tc.error = f'角色 {role} 未定義'
|
|
967
|
+
result.failed += 1
|
|
968
|
+
|
|
969
|
+
result.test_cases.append(tc)
|
|
970
|
+
|
|
971
|
+
# 2. 各模組 UI 元素測試
|
|
972
|
+
ui_elements = {
|
|
973
|
+
'work-orders': ['工單', 'orderNumber', 'productModel'],
|
|
974
|
+
'dispatches': ['派工', 'stationName', 'operatorName'],
|
|
975
|
+
'reports': ['報工', 'goodQty', 'ngQty'],
|
|
976
|
+
'outgassing': ['釋氣', 'signature', 'result'],
|
|
977
|
+
'aoi': ['AOI', 'formAoi', 'aoiCsv'],
|
|
978
|
+
'r0': ['標籤', 'rfidCode', 'epc'],
|
|
979
|
+
'schedule': ['排程', 'schedulePlan', 'shift'],
|
|
980
|
+
'wms': ['倉儲', 'locationCode', 'inventory'],
|
|
981
|
+
'parts': ['物料', 'partNumber', 'partName'],
|
|
982
|
+
'audit': ['稽核', 'auditLog', 'action'],
|
|
983
|
+
'settings': ['設定', 'operator', 'customer'],
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
for module_id, keywords in ui_elements.items():
|
|
987
|
+
module_name = MODULE_APIS.get(module_id, {}).get('name', module_id)
|
|
988
|
+
test_id = f'UAT-UI-{module_id.upper()[:3]}'
|
|
989
|
+
tc = TestCase(id=test_id, name=f'{module_name} UI 元素', module=module_name)
|
|
990
|
+
|
|
991
|
+
tab_file = self.project_path / f'tab-{module_id}.html'
|
|
992
|
+
if tab_file.exists():
|
|
993
|
+
content = tab_file.read_text(encoding='utf-8')
|
|
994
|
+
found = [kw for kw in keywords if kw.lower() in content.lower()]
|
|
995
|
+
if len(found) >= 2:
|
|
996
|
+
tc.status = 'passed'
|
|
997
|
+
tc.details = f'包含關鍵字: {", ".join(found[:3])}'
|
|
998
|
+
result.passed += 1
|
|
999
|
+
else:
|
|
1000
|
+
tc.status = 'failed'
|
|
1001
|
+
tc.error = f'缺少關鍵 UI 元素'
|
|
1002
|
+
result.failed += 1
|
|
1003
|
+
else:
|
|
1004
|
+
tc.status = 'skipped'
|
|
1005
|
+
tc.details = f'tab-{module_id}.html 不存在'
|
|
1006
|
+
result.skipped += 1
|
|
1007
|
+
|
|
1008
|
+
result.test_cases.append(tc)
|
|
1009
|
+
|
|
1010
|
+
# 3. ISO 27001 合規測試
|
|
1011
|
+
iso_tests = [
|
|
1012
|
+
('UAT-ISO-01', 'ISO 27001', '操作紀錄 (A.8.15)', 'getAuditLogs' in self.discovered_apis),
|
|
1013
|
+
('UAT-ISO-02', 'ISO 27001', '存取控制 (A.9.2)', len(ROLE_PERMISSIONS) >= 6),
|
|
1014
|
+
('UAT-ISO-03', 'ISO 27001', '身份驗證 (A.9.4)', 'verifyPin' in self.app_content),
|
|
1015
|
+
('UAT-ISO-04', 'ISO 27001', '資料完整性 (A.14.1)', 'syncDatabase' in self.discovered_apis),
|
|
1016
|
+
]
|
|
1017
|
+
|
|
1018
|
+
for test_id, module, test_name, condition in iso_tests:
|
|
1019
|
+
tc = TestCase(id=test_id, name=test_name, module=module)
|
|
1020
|
+
if condition:
|
|
1021
|
+
tc.status = 'passed'
|
|
1022
|
+
tc.details = '符合要求'
|
|
1023
|
+
result.passed += 1
|
|
1024
|
+
else:
|
|
1025
|
+
tc.status = 'failed'
|
|
1026
|
+
tc.error = '不符合要求'
|
|
1027
|
+
result.failed += 1
|
|
1028
|
+
result.test_cases.append(tc)
|
|
1029
|
+
|
|
1030
|
+
result.duration = (datetime.now() - start_time).total_seconds()
|
|
1031
|
+
result.success = result.failed == 0
|
|
1032
|
+
return result
|
|
1033
|
+
|
|
1034
|
+
# ========== 執行所有測試 ==========
|
|
1035
|
+
|
|
1036
|
+
def run_all(self, test_types: List[str] = None) -> GASMESTestResult:
|
|
1037
|
+
"""執行所有測試類型"""
|
|
1038
|
+
if test_types is None:
|
|
1039
|
+
test_types = ['UIT', 'Smoke', 'E2E', 'UAT']
|
|
1040
|
+
|
|
1041
|
+
result = GASMESTestResult(
|
|
1042
|
+
project_name='GAS MES',
|
|
1043
|
+
timestamp=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
1044
|
+
version=self.version,
|
|
1045
|
+
api_count=len(self.discovered_apis),
|
|
1046
|
+
module_count=len(MODULE_APIS)
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
for test_type in test_types:
|
|
1050
|
+
test_type_upper = test_type.upper()
|
|
1051
|
+
|
|
1052
|
+
console.print(f" [dim]執行 {test_type_upper} 測試...[/dim]")
|
|
1053
|
+
|
|
1054
|
+
if test_type_upper == 'UIT':
|
|
1055
|
+
type_result = self.run_uit()
|
|
1056
|
+
elif test_type_upper == 'SMOKE':
|
|
1057
|
+
type_result = self.run_smoke()
|
|
1058
|
+
elif test_type_upper == 'E2E':
|
|
1059
|
+
type_result = self.run_e2e()
|
|
1060
|
+
elif test_type_upper == 'UAT':
|
|
1061
|
+
type_result = self.run_uat()
|
|
1062
|
+
else:
|
|
1063
|
+
continue
|
|
1064
|
+
|
|
1065
|
+
result.results[test_type_upper] = type_result
|
|
1066
|
+
result.total_passed += type_result.passed
|
|
1067
|
+
result.total_failed += type_result.failed
|
|
1068
|
+
result.total_duration += type_result.duration
|
|
1069
|
+
|
|
1070
|
+
if not type_result.success:
|
|
1071
|
+
result.overall_success = False
|
|
1072
|
+
|
|
1073
|
+
return result
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def render_gas_test_result(result: GASMESTestResult):
|
|
1077
|
+
"""渲染 GAS MES 測試結果"""
|
|
1078
|
+
status_icon = "v" if result.overall_success else "x"
|
|
1079
|
+
status_color = "green" if result.overall_success else "red"
|
|
1080
|
+
|
|
1081
|
+
console.print(Panel(
|
|
1082
|
+
f"[bold]{result.project_name}[/bold] v{result.version} 測試報告\n"
|
|
1083
|
+
f"[dim]{result.timestamp} | {result.api_count} APIs | {result.module_count} 模組[/dim]",
|
|
1084
|
+
border_style="cyan"
|
|
1085
|
+
))
|
|
1086
|
+
|
|
1087
|
+
# 結果表格
|
|
1088
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
1089
|
+
table.add_column("測試類型", style="white")
|
|
1090
|
+
table.add_column("狀態", justify="center")
|
|
1091
|
+
table.add_column("通過", justify="right", style="green")
|
|
1092
|
+
table.add_column("失敗", justify="right", style="red")
|
|
1093
|
+
table.add_column("跳過", justify="right", style="yellow")
|
|
1094
|
+
table.add_column("時間", justify="right", style="dim")
|
|
1095
|
+
|
|
1096
|
+
type_labels = {
|
|
1097
|
+
'UIT': '靜態分析 (UIT)',
|
|
1098
|
+
'SMOKE': '煙霧測試 (Smoke)',
|
|
1099
|
+
'E2E': '功能測試 (E2E)',
|
|
1100
|
+
'UAT': '驗收測試 (UAT)'
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
for test_type, type_result in result.results.items():
|
|
1104
|
+
status = "[green]PASS[/green]" if type_result.success else "[red]FAIL[/red]"
|
|
1105
|
+
table.add_row(
|
|
1106
|
+
type_labels.get(test_type, test_type),
|
|
1107
|
+
status,
|
|
1108
|
+
str(type_result.passed),
|
|
1109
|
+
str(type_result.failed) if type_result.failed else "-",
|
|
1110
|
+
str(type_result.skipped) if type_result.skipped else "-",
|
|
1111
|
+
f"{type_result.duration:.1f}s"
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
console.print(table)
|
|
1115
|
+
|
|
1116
|
+
# 總結
|
|
1117
|
+
total = result.total_passed + result.total_failed
|
|
1118
|
+
if total > 0:
|
|
1119
|
+
pass_rate = (result.total_passed / total) * 100
|
|
1120
|
+
console.print(f"\n 總計: [green]{result.total_passed} passed[/green] / "
|
|
1121
|
+
f"[{'red' if result.total_failed else 'dim'}]{result.total_failed} failed[/]"
|
|
1122
|
+
f" | 通過率: {pass_rate:.1f}% | {result.total_duration:.1f}s")
|
|
1123
|
+
|
|
1124
|
+
if result.overall_success:
|
|
1125
|
+
console.print(f"\n [{status_color}][{status_icon}] 所有測試通過[/{status_color}]\n")
|
|
1126
|
+
else:
|
|
1127
|
+
console.print(f"\n [{status_color}][{status_icon}] 部分測試失敗[/{status_color}]\n")
|
|
1128
|
+
# 顯示失敗詳情 (最多 10 個)
|
|
1129
|
+
failed_count = 0
|
|
1130
|
+
for test_type, type_result in result.results.items():
|
|
1131
|
+
if not type_result.success:
|
|
1132
|
+
for tc in type_result.test_cases:
|
|
1133
|
+
if tc.status == 'failed' and failed_count < 10:
|
|
1134
|
+
console.print(f" [red]x[/red] {tc.id}: {tc.name}")
|
|
1135
|
+
console.print(f" [dim]{tc.error[:80]}[/dim]")
|
|
1136
|
+
failed_count += 1
|
|
1137
|
+
|
|
1138
|
+
return {
|
|
1139
|
+
'success': result.overall_success,
|
|
1140
|
+
'total_passed': result.total_passed,
|
|
1141
|
+
'total_failed': result.total_failed,
|
|
1142
|
+
'version': result.version,
|
|
1143
|
+
'api_count': result.api_count
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def run_gas_test(project_path: str, test_types: List[str] = None, url: str = None) -> Dict:
|
|
1148
|
+
"""執行 GAS MES 測試"""
|
|
1149
|
+
runner = GASMESTestRunner(project_path, url)
|
|
1150
|
+
|
|
1151
|
+
console.print(f"[cyan]GAS MES 完整測試套件[/cyan]")
|
|
1152
|
+
console.print(f"[dim] 專案: {project_path}[/dim]")
|
|
1153
|
+
console.print(f"[dim] 發現 {len(runner.discovered_apis)} 個 API[/dim]")
|
|
1154
|
+
console.print()
|
|
1155
|
+
|
|
1156
|
+
result = runner.run_all(test_types)
|
|
1157
|
+
return render_gas_test_result(result)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def run_gas_test_with_report(
|
|
1161
|
+
project_path: str,
|
|
1162
|
+
output_path: str = None,
|
|
1163
|
+
test_types: List[str] = None,
|
|
1164
|
+
url: str = None
|
|
1165
|
+
) -> Dict:
|
|
1166
|
+
"""執行 GAS MES 測試並產生 Word 報告"""
|
|
1167
|
+
from .word_report import generate_word_report
|
|
1168
|
+
|
|
1169
|
+
runner = GASMESTestRunner(project_path, url)
|
|
1170
|
+
|
|
1171
|
+
console.print(f"[cyan]GAS MES 完整測試套件[/cyan]")
|
|
1172
|
+
console.print(f"[dim] 專案: {project_path}[/dim]")
|
|
1173
|
+
console.print(f"[dim] 發現 {len(runner.discovered_apis)} 個 API[/dim]")
|
|
1174
|
+
console.print()
|
|
1175
|
+
|
|
1176
|
+
result = runner.run_all(test_types)
|
|
1177
|
+
render_gas_test_result(result)
|
|
1178
|
+
|
|
1179
|
+
# 準備報告資料
|
|
1180
|
+
if not output_path:
|
|
1181
|
+
output_path = str(Path(project_path) / 'GAS_MES-test-report.docx')
|
|
1182
|
+
|
|
1183
|
+
test_results = {
|
|
1184
|
+
'project': f'{result.project_name} v{result.version}',
|
|
1185
|
+
'timestamp': result.timestamp,
|
|
1186
|
+
'overall_success': result.overall_success,
|
|
1187
|
+
'summary': {
|
|
1188
|
+
'total_passed': result.total_passed,
|
|
1189
|
+
'total_failed': result.total_failed,
|
|
1190
|
+
'total_duration': result.total_duration,
|
|
1191
|
+
'coverage': 0
|
|
1192
|
+
},
|
|
1193
|
+
'tests': {
|
|
1194
|
+
k: {
|
|
1195
|
+
'success': v.success,
|
|
1196
|
+
'passed': v.passed,
|
|
1197
|
+
'failed': v.failed,
|
|
1198
|
+
'duration': v.duration,
|
|
1199
|
+
'coverage': 0,
|
|
1200
|
+
'not_configured': v.not_configured,
|
|
1201
|
+
'test_cases': [
|
|
1202
|
+
{
|
|
1203
|
+
'name': f"[{tc.module}] {tc.name}" if tc.module else tc.name,
|
|
1204
|
+
'status': tc.status,
|
|
1205
|
+
'duration': tc.duration,
|
|
1206
|
+
'error': tc.error,
|
|
1207
|
+
'screenshot': tc.screenshot,
|
|
1208
|
+
'api_response': '',
|
|
1209
|
+
'terminal_output': tc.details
|
|
1210
|
+
}
|
|
1211
|
+
for tc in v.test_cases
|
|
1212
|
+
]
|
|
1213
|
+
}
|
|
1214
|
+
for k, v in result.results.items()
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
# 收集截圖
|
|
1219
|
+
screenshots = []
|
|
1220
|
+
for type_result in result.results.values():
|
|
1221
|
+
for tc in type_result.test_cases:
|
|
1222
|
+
if tc.screenshot and Path(tc.screenshot).exists():
|
|
1223
|
+
screenshots.append(tc.screenshot)
|
|
1224
|
+
|
|
1225
|
+
console.print(f"\n[cyan]產生 Word 報告... (收集到 {len(screenshots)} 張截圖)[/cyan]")
|
|
1226
|
+
report_path = generate_word_report(
|
|
1227
|
+
project_name=f'{result.project_name} v{result.version}',
|
|
1228
|
+
test_results=test_results,
|
|
1229
|
+
output_path=output_path,
|
|
1230
|
+
screenshots=screenshots, # 包含所有模組截圖
|
|
1231
|
+
include_charts=True
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
console.print(f"[green]報告已產生: {report_path}[/green]")
|
|
1235
|
+
|
|
1236
|
+
return {
|
|
1237
|
+
'success': result.overall_success,
|
|
1238
|
+
'report_path': report_path,
|
|
1239
|
+
'total_passed': result.total_passed,
|
|
1240
|
+
'total_failed': result.total_failed
|
|
1241
|
+
}
|