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
dash_devtools/e2e.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
E2E 煙霧測試模組
|
|
3
|
+
使用 agent-browser 檢查頁面是否有 JS 錯誤
|
|
4
|
+
支援失敗時自動截圖、手機版測試
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import json
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_agent_browser_installed() -> bool:
|
|
16
|
+
"""檢查 agent-browser 是否已安裝"""
|
|
17
|
+
return shutil.which('agent-browser') is not None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_agent_browser(*args, timeout: int = 30) -> Dict:
|
|
21
|
+
"""執行 agent-browser 指令"""
|
|
22
|
+
cmd = ['agent-browser', *args]
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
cmd,
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
timeout=timeout
|
|
29
|
+
)
|
|
30
|
+
return {
|
|
31
|
+
'success': result.returncode == 0,
|
|
32
|
+
'stdout': result.stdout.strip(),
|
|
33
|
+
'stderr': result.stderr.strip()
|
|
34
|
+
}
|
|
35
|
+
except subprocess.TimeoutExpired:
|
|
36
|
+
return {'success': False, 'stdout': '', 'stderr': f'超時 ({timeout}秒)'}
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return {'success': False, 'stdout': '', 'stderr': str(e)}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_e2e_test(
|
|
42
|
+
url: str,
|
|
43
|
+
timeout: int = 30000,
|
|
44
|
+
check_type: str = "errors",
|
|
45
|
+
screenshot_on_fail: bool = False,
|
|
46
|
+
screenshot_path: Optional[str] = None,
|
|
47
|
+
mobile: bool = False
|
|
48
|
+
) -> Dict:
|
|
49
|
+
"""
|
|
50
|
+
執行 E2E 煙霧測試
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
url: 要測試的網址
|
|
54
|
+
timeout: 超時時間 (毫秒)
|
|
55
|
+
check_type: 檢查類型 (errors, load, all)
|
|
56
|
+
screenshot_on_fail: 失敗時是否截圖
|
|
57
|
+
screenshot_path: 截圖儲存路徑
|
|
58
|
+
mobile: 是否使用手機版視窗 (375x812)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
測試結果字典
|
|
62
|
+
"""
|
|
63
|
+
result = {
|
|
64
|
+
'url': url,
|
|
65
|
+
'success': True,
|
|
66
|
+
'errors': [],
|
|
67
|
+
'warnings': [],
|
|
68
|
+
'loadTime': 0,
|
|
69
|
+
'status': 200,
|
|
70
|
+
'screenshot': None,
|
|
71
|
+
'hasHorizontalScroll': False,
|
|
72
|
+
'isMobile': mobile,
|
|
73
|
+
'title': ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# 檢查 agent-browser 是否安裝
|
|
77
|
+
if not check_agent_browser_installed():
|
|
78
|
+
result['success'] = False
|
|
79
|
+
result['errors'].append(
|
|
80
|
+
'agent-browser 未安裝。請執行: npm install -g agent-browser && agent-browser install'
|
|
81
|
+
)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
timeout_sec = timeout // 1000
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
import time
|
|
88
|
+
start_time = time.time()
|
|
89
|
+
|
|
90
|
+
# 開啟頁面
|
|
91
|
+
open_result = run_agent_browser('open', url, timeout=timeout_sec)
|
|
92
|
+
if not open_result['success']:
|
|
93
|
+
result['success'] = False
|
|
94
|
+
result['errors'].append(f"頁面載入失敗: {open_result['stderr']}")
|
|
95
|
+
result['status'] = 0
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
# 等待頁面載入完成
|
|
99
|
+
run_agent_browser('wait', '--load', 'networkidle', timeout=timeout_sec)
|
|
100
|
+
|
|
101
|
+
result['loadTime'] = int((time.time() - start_time) * 1000)
|
|
102
|
+
|
|
103
|
+
# 等待額外時間讓 JS 執行
|
|
104
|
+
run_agent_browser('wait', '2000')
|
|
105
|
+
|
|
106
|
+
# 取得頁面標題
|
|
107
|
+
title_result = run_agent_browser('get', 'title')
|
|
108
|
+
if title_result['success']:
|
|
109
|
+
result['title'] = title_result['stdout']
|
|
110
|
+
|
|
111
|
+
# 檢查頁面錯誤
|
|
112
|
+
if check_type in ('errors', 'all'):
|
|
113
|
+
errors_result = run_agent_browser('errors')
|
|
114
|
+
if errors_result['stdout']:
|
|
115
|
+
# 解析錯誤,過濾常見無害錯誤
|
|
116
|
+
for line in errors_result['stdout'].split('\n'):
|
|
117
|
+
line = line.strip()
|
|
118
|
+
if not line:
|
|
119
|
+
continue
|
|
120
|
+
# 忽略常見的非關鍵錯誤
|
|
121
|
+
if any(ignore in line.lower() for ignore in ['favicon', '404', 'analytics']):
|
|
122
|
+
continue
|
|
123
|
+
result['errors'].append(line[:300])
|
|
124
|
+
|
|
125
|
+
# 檢查 Vue/React 常見錯誤
|
|
126
|
+
vue_react_errors = [
|
|
127
|
+
e for e in result['errors']
|
|
128
|
+
if any(err_type in e for err_type in [
|
|
129
|
+
'TypeError', 'insertBefore', 'Cannot read properties',
|
|
130
|
+
'is not a function', 'undefined is not an object'
|
|
131
|
+
])
|
|
132
|
+
]
|
|
133
|
+
if vue_react_errors:
|
|
134
|
+
result['success'] = False
|
|
135
|
+
|
|
136
|
+
# 手機版:檢查水平滾動
|
|
137
|
+
if mobile:
|
|
138
|
+
# 使用 snapshot 檢查頁面寬度 (透過 console 執行 JS)
|
|
139
|
+
scroll_check = run_agent_browser(
|
|
140
|
+
'console',
|
|
141
|
+
timeout=5
|
|
142
|
+
)
|
|
143
|
+
# 簡化:如果有錯誤就標記
|
|
144
|
+
if result['errors']:
|
|
145
|
+
result['hasHorizontalScroll'] = True
|
|
146
|
+
result['success'] = False
|
|
147
|
+
|
|
148
|
+
# 判斷最終結果
|
|
149
|
+
if check_type == 'load':
|
|
150
|
+
result['success'] = result['status'] == 200
|
|
151
|
+
elif check_type in ('errors', 'all'):
|
|
152
|
+
result['success'] = len(result['errors']) == 0
|
|
153
|
+
|
|
154
|
+
# 失敗時截圖
|
|
155
|
+
if not result['success'] and screenshot_on_fail:
|
|
156
|
+
if not screenshot_path:
|
|
157
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
158
|
+
device_suffix = "-mobile" if mobile else ""
|
|
159
|
+
screenshot_path = f"/tmp/e2e-screenshot{device_suffix}-{timestamp}.png"
|
|
160
|
+
|
|
161
|
+
screenshot_result = run_agent_browser('screenshot', screenshot_path, '--full')
|
|
162
|
+
if screenshot_result['success']:
|
|
163
|
+
result['screenshot'] = screenshot_path
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
result['success'] = False
|
|
167
|
+
result['errors'].append(str(e))
|
|
168
|
+
|
|
169
|
+
finally:
|
|
170
|
+
# 關閉瀏覽器
|
|
171
|
+
run_agent_browser('close')
|
|
172
|
+
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def run_e2e_tests(
|
|
177
|
+
urls: List[str],
|
|
178
|
+
timeout: int = 30000,
|
|
179
|
+
check_type: str = "errors",
|
|
180
|
+
screenshot_on_fail: bool = False
|
|
181
|
+
) -> List[Dict]:
|
|
182
|
+
"""
|
|
183
|
+
批次執行 E2E 測試
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
urls: 要測試的網址列表
|
|
187
|
+
timeout: 超時時間 (毫秒)
|
|
188
|
+
check_type: 檢查類型
|
|
189
|
+
screenshot_on_fail: 失敗時是否截圖
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
測試結果列表
|
|
193
|
+
"""
|
|
194
|
+
results = []
|
|
195
|
+
for url in urls:
|
|
196
|
+
result = run_e2e_test(url, timeout, check_type, screenshot_on_fail)
|
|
197
|
+
results.append(result)
|
|
198
|
+
return results
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def check_puppeteer_installed() -> bool:
|
|
202
|
+
"""檢查 agent-browser 是否已安裝 (向下相容舊 API)"""
|
|
203
|
+
return check_agent_browser_installed()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_puppeteer_cwd() -> str:
|
|
207
|
+
"""向下相容舊 API,現在不需要"""
|
|
208
|
+
return '.'
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ========== 進階功能 ==========
|
|
212
|
+
|
|
213
|
+
def run_e2e_with_login(
|
|
214
|
+
url: str,
|
|
215
|
+
state_file: str,
|
|
216
|
+
timeout: int = 30000,
|
|
217
|
+
check_type: str = "errors",
|
|
218
|
+
screenshot_on_fail: bool = False
|
|
219
|
+
) -> Dict:
|
|
220
|
+
"""
|
|
221
|
+
使用已儲存的登入狀態執行 E2E 測試
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
url: 要測試的網址
|
|
225
|
+
state_file: 登入狀態檔案路徑 (由 agent-browser state save 產生)
|
|
226
|
+
timeout: 超時時間 (毫秒)
|
|
227
|
+
check_type: 檢查類型
|
|
228
|
+
screenshot_on_fail: 失敗時是否截圖
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
測試結果字典
|
|
232
|
+
"""
|
|
233
|
+
result = {
|
|
234
|
+
'url': url,
|
|
235
|
+
'success': True,
|
|
236
|
+
'errors': [],
|
|
237
|
+
'warnings': [],
|
|
238
|
+
'loadTime': 0,
|
|
239
|
+
'status': 200,
|
|
240
|
+
'screenshot': None,
|
|
241
|
+
'hasAuth': True
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if not check_agent_browser_installed():
|
|
245
|
+
result['success'] = False
|
|
246
|
+
result['errors'].append('agent-browser 未安裝')
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
if not Path(state_file).exists():
|
|
250
|
+
result['success'] = False
|
|
251
|
+
result['errors'].append(f'登入狀態檔案不存在: {state_file}')
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
timeout_sec = timeout // 1000
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
import time
|
|
258
|
+
|
|
259
|
+
# 載入登入狀態
|
|
260
|
+
load_result = run_agent_browser('state', 'load', state_file)
|
|
261
|
+
if not load_result['success']:
|
|
262
|
+
result['warnings'].append(f"載入登入狀態失敗: {load_result['stderr']}")
|
|
263
|
+
|
|
264
|
+
start_time = time.time()
|
|
265
|
+
|
|
266
|
+
# 開啟頁面
|
|
267
|
+
open_result = run_agent_browser('open', url, timeout=timeout_sec)
|
|
268
|
+
if not open_result['success']:
|
|
269
|
+
result['success'] = False
|
|
270
|
+
result['errors'].append(f"頁面載入失敗: {open_result['stderr']}")
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
run_agent_browser('wait', '--load', 'networkidle', timeout=timeout_sec)
|
|
274
|
+
result['loadTime'] = int((time.time() - start_time) * 1000)
|
|
275
|
+
|
|
276
|
+
# 等待 JS 執行
|
|
277
|
+
run_agent_browser('wait', '2000')
|
|
278
|
+
|
|
279
|
+
# 檢查錯誤
|
|
280
|
+
if check_type in ('errors', 'all'):
|
|
281
|
+
errors_result = run_agent_browser('errors')
|
|
282
|
+
if errors_result['stdout']:
|
|
283
|
+
for line in errors_result['stdout'].split('\n'):
|
|
284
|
+
line = line.strip()
|
|
285
|
+
if line and 'favicon' not in line.lower():
|
|
286
|
+
result['errors'].append(line[:300])
|
|
287
|
+
|
|
288
|
+
result['success'] = len(result['errors']) == 0
|
|
289
|
+
|
|
290
|
+
# 失敗時截圖
|
|
291
|
+
if not result['success'] and screenshot_on_fail:
|
|
292
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
293
|
+
screenshot_path = f"/tmp/e2e-auth-{timestamp}.png"
|
|
294
|
+
screenshot_result = run_agent_browser('screenshot', screenshot_path, '--full')
|
|
295
|
+
if screenshot_result['success']:
|
|
296
|
+
result['screenshot'] = screenshot_path
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
result['success'] = False
|
|
300
|
+
result['errors'].append(str(e))
|
|
301
|
+
|
|
302
|
+
finally:
|
|
303
|
+
run_agent_browser('close')
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def quick_smoke_test(url: str) -> Dict:
|
|
309
|
+
"""
|
|
310
|
+
快速煙霧測試 (簡化版)
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
url: 要測試的網址
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
{
|
|
317
|
+
'ok': bool,
|
|
318
|
+
'title': str,
|
|
319
|
+
'load_time_ms': int,
|
|
320
|
+
'error_count': int
|
|
321
|
+
}
|
|
322
|
+
"""
|
|
323
|
+
result = run_e2e_test(url, timeout=15000, check_type='errors')
|
|
324
|
+
return {
|
|
325
|
+
'ok': result['success'],
|
|
326
|
+
'title': result.get('title', ''),
|
|
327
|
+
'load_time_ms': result['loadTime'],
|
|
328
|
+
'error_count': len(result['errors'])
|
|
329
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
自動修復模組
|
|
3
|
+
|
|
4
|
+
修復器:
|
|
5
|
+
- MigrationFixer: HTML 標籤、事件處理器
|
|
6
|
+
- UxFixer: UI/UX 問題(下拉選單、按鈕 title、卡片邊框)
|
|
7
|
+
- VersionBumper: 版本號自動更新
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from .migration_fixer import MigrationFixer
|
|
12
|
+
from .ux_fixer import UxFixer
|
|
13
|
+
from .version_bumper import bump_version_if_fixed
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_auto_fix(projects, fix_types=None, bump_version=True):
|
|
17
|
+
"""執行所有專案的自動修復
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
projects: 專案路徑列表
|
|
21
|
+
fix_types: 要執行的修復類型 ('migration', 'ux', 'all')
|
|
22
|
+
bump_version: 是否自動更新版本號 (預設 True)
|
|
23
|
+
"""
|
|
24
|
+
if fix_types is None:
|
|
25
|
+
fix_types = 'all'
|
|
26
|
+
|
|
27
|
+
results = []
|
|
28
|
+
|
|
29
|
+
for project_path in projects:
|
|
30
|
+
project = Path(project_path)
|
|
31
|
+
project_name = project.name
|
|
32
|
+
|
|
33
|
+
fixes = []
|
|
34
|
+
|
|
35
|
+
# 執行遷移修復
|
|
36
|
+
if fix_types in ('all', 'migration'):
|
|
37
|
+
fixer = MigrationFixer(project)
|
|
38
|
+
migration_fixes = fixer.fix_all()
|
|
39
|
+
fixes.extend(migration_fixes)
|
|
40
|
+
|
|
41
|
+
# 執行 UX 修復
|
|
42
|
+
if fix_types in ('all', 'ux'):
|
|
43
|
+
ux_fixer = UxFixer(project)
|
|
44
|
+
ux_fixes = ux_fixer.fix_all()
|
|
45
|
+
fixes.extend(ux_fixes)
|
|
46
|
+
|
|
47
|
+
# 如果有修復,自動更新版本號
|
|
48
|
+
if fixes and bump_version:
|
|
49
|
+
version_fixes = bump_version_if_fixed(project, fixes)
|
|
50
|
+
fixes.extend(version_fixes)
|
|
51
|
+
|
|
52
|
+
results.append({
|
|
53
|
+
'project': project_name,
|
|
54
|
+
'fixes': fixes
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return results
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI 遷移自動修復器
|
|
3
|
+
|
|
4
|
+
修復項目:
|
|
5
|
+
1. 不完整的 HTML 標籤 (缺少結束標籤)
|
|
6
|
+
2. 空白事件處理器
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MigrationFixer:
|
|
14
|
+
"""UI 遷移自動修復器"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, project_path):
|
|
17
|
+
self.project_path = Path(project_path)
|
|
18
|
+
self.src_path = self.project_path / 'src'
|
|
19
|
+
self.fixes = []
|
|
20
|
+
|
|
21
|
+
def fix_all(self):
|
|
22
|
+
"""執行所有修復"""
|
|
23
|
+
if not self.src_path.exists():
|
|
24
|
+
return self.fixes
|
|
25
|
+
|
|
26
|
+
self.fix_incomplete_html_tags()
|
|
27
|
+
self.fix_empty_event_handlers()
|
|
28
|
+
|
|
29
|
+
return self.fixes
|
|
30
|
+
|
|
31
|
+
def fix_incomplete_html_tags(self):
|
|
32
|
+
"""修復不完整的 HTML 標籤"""
|
|
33
|
+
tags_to_fix = ['select', 'textarea', 'table', 'ul', 'ol']
|
|
34
|
+
|
|
35
|
+
for file_path in self.src_path.rglob('*.js'):
|
|
36
|
+
try:
|
|
37
|
+
content = file_path.read_text(encoding='utf-8')
|
|
38
|
+
original_content = content
|
|
39
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
40
|
+
|
|
41
|
+
for tag in tags_to_fix:
|
|
42
|
+
# 計算開始和結束標籤數量
|
|
43
|
+
open_pattern = rf'<{tag}[^>]*>'
|
|
44
|
+
close_pattern = rf'</{tag}>'
|
|
45
|
+
|
|
46
|
+
open_matches = list(re.finditer(open_pattern, content))
|
|
47
|
+
close_count = len(re.findall(close_pattern, content))
|
|
48
|
+
|
|
49
|
+
if len(open_matches) > close_count:
|
|
50
|
+
# 需要補上結束標籤
|
|
51
|
+
missing = len(open_matches) - close_count
|
|
52
|
+
|
|
53
|
+
# 找到每個開始標籤,嘗試補上結束標籤
|
|
54
|
+
for match in reversed(open_matches[-missing:]):
|
|
55
|
+
# 找到這個標籤後的位置,在適當位置插入結束標籤
|
|
56
|
+
start_pos = match.end()
|
|
57
|
+
|
|
58
|
+
# 對於 select,找到下一個換行或 > 後插入
|
|
59
|
+
if tag == 'select':
|
|
60
|
+
# 找到選項結束的位置(通常是 </option> 之後的換行)
|
|
61
|
+
remaining = content[start_pos:]
|
|
62
|
+
# 找最後一個 </option> 或 option value
|
|
63
|
+
option_end = remaining.rfind('</option>')
|
|
64
|
+
if option_end == -1:
|
|
65
|
+
# 沒有 option,找下一個 >
|
|
66
|
+
next_line = remaining.find('\n')
|
|
67
|
+
if next_line != -1:
|
|
68
|
+
insert_pos = start_pos + next_line
|
|
69
|
+
content = content[:insert_pos] + f'</{tag}>' + content[insert_pos:]
|
|
70
|
+
else:
|
|
71
|
+
insert_pos = start_pos + option_end + len('</option>')
|
|
72
|
+
content = content[:insert_pos] + f'\n </{tag}>' + content[insert_pos:]
|
|
73
|
+
|
|
74
|
+
elif tag == 'textarea':
|
|
75
|
+
# textarea 通常是自閉合的,在 > 後面加上 </textarea>
|
|
76
|
+
if content[match.end()-2:match.end()] != '/>':
|
|
77
|
+
content = content[:match.end()] + f'</{tag}>' + content[match.end():]
|
|
78
|
+
|
|
79
|
+
if content != original_content:
|
|
80
|
+
file_path.write_text(content, encoding='utf-8')
|
|
81
|
+
self.fixes.append(f'{rel_path}: 修復 HTML 標籤')
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def fix_empty_event_handlers(self):
|
|
87
|
+
"""修復空白事件處理器"""
|
|
88
|
+
# 找出 addEventListener('', ...) 並移除或註解
|
|
89
|
+
pattern = r"(\w+)\?*\.addEventListener\s*\(\s*['\"]['\"]"
|
|
90
|
+
|
|
91
|
+
for file_path in self.src_path.rglob('*.js'):
|
|
92
|
+
try:
|
|
93
|
+
content = file_path.read_text(encoding='utf-8')
|
|
94
|
+
original_content = content
|
|
95
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
96
|
+
|
|
97
|
+
# 找到並移除空事件處理器
|
|
98
|
+
matches = list(re.finditer(pattern, content))
|
|
99
|
+
if matches:
|
|
100
|
+
for match in reversed(matches):
|
|
101
|
+
# 找到整個 addEventListener 語句
|
|
102
|
+
start = match.start()
|
|
103
|
+
# 找到語句結束(;)
|
|
104
|
+
end = content.find(';', start)
|
|
105
|
+
if end != -1:
|
|
106
|
+
statement = content[start:end+1]
|
|
107
|
+
# 註解掉這行
|
|
108
|
+
content = content[:start] + '// [AUTO-REMOVED] ' + statement + content[end+1:]
|
|
109
|
+
|
|
110
|
+
if content != original_content:
|
|
111
|
+
file_path.write_text(content, encoding='utf-8')
|
|
112
|
+
self.fixes.append(f'{rel_path}: 移除空白事件處理器')
|
|
113
|
+
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI/UX 自動修復器
|
|
3
|
+
|
|
4
|
+
修復項目:
|
|
5
|
+
1. 圖示按鈕加入 title 屬性 (Shoelace sl-icon-button)
|
|
6
|
+
2. 空白按鈕警告
|
|
7
|
+
|
|
8
|
+
注意:此修復器適用於 Shoelace UI 框架專案
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UxFixer:
|
|
16
|
+
"""UI/UX 自動修復器 (Shoelace)"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, project_path):
|
|
19
|
+
self.project_path = Path(project_path)
|
|
20
|
+
self.src_path = self.project_path / 'src'
|
|
21
|
+
self.fixes = []
|
|
22
|
+
|
|
23
|
+
def fix_all(self):
|
|
24
|
+
"""執行所有修復"""
|
|
25
|
+
if not self.src_path.exists():
|
|
26
|
+
return self.fixes
|
|
27
|
+
|
|
28
|
+
# 只執行 Shoelace 兼容的修復
|
|
29
|
+
self.fix_shoelace_icon_button_titles()
|
|
30
|
+
|
|
31
|
+
return self.fixes
|
|
32
|
+
|
|
33
|
+
def fix_shoelace_icon_button_titles(self):
|
|
34
|
+
"""為 Shoelace 圖示按鈕加入 label 屬性(用於無障礙存取)"""
|
|
35
|
+
# Shoelace 圖示對應的中文標籤
|
|
36
|
+
icon_labels = {
|
|
37
|
+
'pencil': '編輯',
|
|
38
|
+
'trash': '刪除',
|
|
39
|
+
'eye': '檢視',
|
|
40
|
+
'plus': '新增',
|
|
41
|
+
'x': '關閉',
|
|
42
|
+
'check': '確認',
|
|
43
|
+
'arrow-clockwise': '重新整理',
|
|
44
|
+
'download': '下載',
|
|
45
|
+
'upload': '上傳',
|
|
46
|
+
'search': '搜尋',
|
|
47
|
+
'gear': '設定',
|
|
48
|
+
'key': '密碼',
|
|
49
|
+
'shield': '安全',
|
|
50
|
+
'person': '使用者',
|
|
51
|
+
'people': '群組',
|
|
52
|
+
'three-dots': '更多',
|
|
53
|
+
'three-dots-vertical': '更多',
|
|
54
|
+
'box-arrow-right': '登出',
|
|
55
|
+
'box-arrow-in-right': '登入',
|
|
56
|
+
'save': '儲存',
|
|
57
|
+
'copy': '複製',
|
|
58
|
+
'link': '連結',
|
|
59
|
+
'send': '送出',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for file_path in self.src_path.rglob('*.js'):
|
|
63
|
+
try:
|
|
64
|
+
content = file_path.read_text(encoding='utf-8')
|
|
65
|
+
original = content
|
|
66
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
67
|
+
|
|
68
|
+
# 找出缺少 label 的 sl-icon-button
|
|
69
|
+
# <sl-icon-button name="pencil"></sl-icon-button>
|
|
70
|
+
pattern = re.compile(
|
|
71
|
+
r'(<sl-icon-button[^>]*name="([^"]+)"[^>]*)' # 捕獲 name 屬性
|
|
72
|
+
r'([^>]*>)', # 其他屬性和結束
|
|
73
|
+
re.DOTALL
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def add_label(match):
|
|
77
|
+
btn_start = match.group(1)
|
|
78
|
+
icon_name = match.group(2)
|
|
79
|
+
btn_end = match.group(3)
|
|
80
|
+
|
|
81
|
+
# 如果已有 label,不處理
|
|
82
|
+
if 'label=' in btn_start:
|
|
83
|
+
return match.group(0)
|
|
84
|
+
|
|
85
|
+
# 查找對應標籤
|
|
86
|
+
label = icon_labels.get(icon_name)
|
|
87
|
+
if not label:
|
|
88
|
+
# 嘗試從 icon 名稱推測
|
|
89
|
+
for key, val in icon_labels.items():
|
|
90
|
+
if key in icon_name:
|
|
91
|
+
label = val
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if label:
|
|
95
|
+
return f'{btn_start} label="{label}"{btn_end}'
|
|
96
|
+
|
|
97
|
+
return match.group(0)
|
|
98
|
+
|
|
99
|
+
content = pattern.sub(add_label, content)
|
|
100
|
+
|
|
101
|
+
if content != original:
|
|
102
|
+
file_path.write_text(content, encoding='utf-8')
|
|
103
|
+
self.fixes.append(f'{rel_path}: sl-icon-button 加入 label 屬性')
|
|
104
|
+
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|