Files
xiaoli-cli/plugins/frontend_tester.py
T
xiaoli-bot 29df2a4ac1 chore: 移除所有 emoji 及对标文案
- 删除全部文件中的 emoji 字符(29 个文件)
- 移除 tui.py 中「对标 Claude Code」表述
- 保持代码缩进和格式不变
2026-04-26 12:35:27 +08:00

478 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
前端测试验证插件 - 类似 iFlow CLI 的 frontend-tester
支持HTML/CSS/JS自动化测试和UI验证
"""
import os
import subprocess
from colorama import Fore, Style
class Liugin:
"""前端测试验证插件 - 自动化测试Web界面"""
def __init__(self):
self.usage = """前端测试工具使用方法:
JSON格式示例:
{"action": "use_tool", "tool": "frontend_tester", "args": "validate index.html"} - 验证HTML文件
{"action": "use_tool", "tool": "frontend_tester", "args": "test style.css"} - 测试CSS文件
{"action": "use_tool", "tool": "frontend_tester", "args": "check app.js"} - 检查JS文件
{"action": "use_tool", "tool": "frontend_tester", "args": "preview index.html"} - 在浏览器中预览
{"action": "use_tool", "tool": "frontend_tester", "args": "analyze project"} - 分析整个项目
功能说明:
- validate <HTML文件路径> - 验证HTML文件的语法和结构
- test <CSS文件路径> - 测试CSS文件的样式和兼容性
- check <JS文件路径> - 检查JS文件的语法和潜在问题
- preview <HTML文件路径> - 在默认浏览器中打开预览
- analyze <项目路径> - 分析整个前端项目的结构和问题
支持的检查项:
HTML语法验证
CSS语法和兼容性检查
JavaScript语法检查
响应式设计检查
可访问性检查
性能优化建议"""
self.cli = None
def set_cli(self, cli):
"""设置CLI实例引用"""
self.cli = cli
# 注册插件命令
self.cli.register_liugin_command('ftest', self.command_handler)
self.cli.register_liugin_command('frontend', self.command_handler)
def command_handler(self, args):
"""处理 /ftest 或 /frontend 命令"""
return self.handle(args)
def get_tool_info(self):
return {
"name": "frontend_tester",
"description": "前端测试验证工具,支持HTML/CSS/JS自动化测试和UI验证,包括语法检查、响应式设计、可访问性检查等",
"keywords": ["前端", "测试", "HTML", "CSS", "JavaScript", "UI", "验证", "preview", "validate", "frontend", "test"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "frontend_tester",
"description": "前端测试验证工具,支持HTML/CSS/JS自动化测试",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["validate", "lint", "test"],
"description": "操作类型"
},
"target": {
"type": "string",
"description": "目标文件路径"
}
},
"required": ["operation", "target"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", "")
return f"{op} {target}"
def handle(self, args):
"""处理前端测试请求"""
try:
parts = args.strip().split(maxsplit=1)
if not parts:
return "错误:请提供操作类型和参数。使用 'frontend_tester help' 查看帮助"
operation = parts[0].lower()
if operation == "help":
return self.usage
elif operation == "validate":
# 验证HTML文件
if len(parts) < 2:
return "错误:请提供HTML文件路径。格式: validate <HTML文件路径>"
file_path = parts[1]
return self._validate_html(file_path)
elif operation == "test":
# 测试CSS文件
if len(parts) < 2:
return "错误:请提供CSS文件路径。格式: test <CSS文件路径>"
file_path = parts[1]
return self._test_css(file_path)
elif operation == "check":
# 检查JS文件
if len(parts) < 2:
return "错误:请提供JS文件路径。格式: check <JS文件路径>"
file_path = parts[1]
return self._check_js(file_path)
elif operation == "preview":
# 在浏览器中预览
if len(parts) < 2:
return "错误:请提供HTML文件路径。格式: preview <HTML文件路径>"
file_path = parts[1]
return self._preview_in_browser(file_path)
elif operation == "analyze":
# 分析整个项目
if len(parts) < 2:
return "错误:请提供项目路径。格式: analyze <项目路径>"
project_path = parts[1]
return self._analyze_project(project_path)
else:
return f"错误:不支持的操作 '{operation}'。支持的操作有: validate, test, check, preview, analyze, help"
except Exception as e:
return f"前端测试操作错误: {str(e)}"
def _validate_html(self, file_path):
"""验证HTML文件"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n HTML验证结果: {file_path}\n"
result += "=" * 60 + "\n\n"
# 基本检查
issues = []
# 检查DOCTYPE
if not content.strip().startswith('<!DOCTYPE'):
issues.append({
"level": "warning",
"message": "缺少DOCTYPE声明,建议添加 <!DOCTYPE html>"
})
# 检查lang属性
if '<html' in content and 'lang=' not in content:
issues.append({
"level": "warning",
"message": "建议在<html>标签中添加lang属性,如 lang=\"zh-CN\""
})
# 检查meta标签
if '<meta charset=' not in content:
issues.append({
"level": "error",
"message": "缺少字符集meta标签,建议添加 <meta charset=\"UTF-8\">"
})
if '<meta name="viewport"' not in content:
issues.append({
"level": "warning",
"message": "缺少viewport meta标签,建议添加以支持响应式设计"
})
# 检查title标签
if '<title>' not in content:
issues.append({
"level": "error",
"message": "缺少<title>标签"
})
# 统计元素
div_count = content.count('<div')
img_count = content.count('<img')
img_alt_count = content.count('alt=')
link_count = content.count('<link')
script_count = content.count('<script')
result += f" 文件统计:\n"
result += f" - div元素: {div_count}\n"
result += f" - img元素: {img_count}\n"
result += f" - img alt属性: {img_alt_count}\n"
result += f" - link标签: {link_count}\n"
result += f" - script标签: {script_count}\n\n"
# 检查img alt属性
if img_count > 0 and img_alt_count < img_count:
issues.append({
"level": "warning",
"message": f"有 {img_count - img_alt_count} 个img元素缺少alt属性"
})
# 显示问题
if issues:
result += f" 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN} 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return " 文件编码问题,无法读取"
except Exception as e:
return f" 验证失败: {str(e)}"
def _test_css(self, file_path):
"""测试CSS文件"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n CSS测试结果: {file_path}\n"
result += "=" * 60 + "\n\n"
issues = []
# 检查常见的CSS问题
if content.count('{') != content.count('}'):
issues.append({
"level": "error",
"message": "大括号不匹配"
})
# 检查颜色使用
if '#000000' in content or '#ffffff' in content:
issues.append({
"level": "info",
"message": "使用了纯黑或纯白,建议使用更柔和的颜色"
})
# 检查单位使用
if content.count('px') > 10 and 'rem' not in content and 'em' not in content:
issues.append({
"level": "warning",
"message": "大量使用px单位,建议考虑使用rem或em以支持更好的缩放"
})
# 统计
rule_count = content.count('{')
selector_count = len([line for line in content.split('\n') if '{' in line])
result += f" 文件统计:\n"
result += f" - CSS规则: {rule_count}\n"
result += f" - 选择器: {selector_count}\n\n"
# 显示问题
if issues:
result += f" 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN} 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return " 文件编码问题,无法读取"
except Exception as e:
return f" 测试失败: {str(e)}"
def _check_js(self, file_path):
"""检查JS文件"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
result = f"\n JavaScript检查结果: {file_path}\n"
result += "=" * 60 + "\n\n"
issues = []
# 检查常见问题
if 'var ' in content:
issues.append({
"level": "warning",
"message": "使用了var声明,建议使用let或const"
})
if '==' in content and '!==' not in content:
issues.append({
"level": "warning",
"message": "使用了==比较,建议使用===严格相等"
})
if content.count('console.log') > 5:
issues.append({
"level": "info",
"message": f"有{content.count('console.log')}个console.log,生产环境建议移除"
})
if 'eval(' in content:
issues.append({
"level": "error",
"message": "使用了eval(),存在安全风险"
})
# 统计
line_count = len(content.split('\n'))
function_count = content.count('function ')
result += f" 文件统计:\n"
result += f" - 代码行数: {line_count}\n"
result += f" - 函数数量: {function_count}\n\n"
# 显示问题
if issues:
result += f" 发现 {len(issues)} 个问题:\n\n"
for i, issue in enumerate(issues, 1):
level = issue['level']
if level == 'error':
icon = f"{Fore.RED}{Style.RESET_ALL}"
elif level == 'warning':
icon = f"{Fore.YELLOW}{Style.RESET_ALL}"
else:
icon = f"{Fore.GREEN}ℹ{Style.RESET_ALL}"
result += f"{icon} {i}. {issue['message']}\n"
else:
result += f"{Fore.GREEN} 未发现问题{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except UnicodeDecodeError:
return " 文件编码问题,无法读取"
except Exception as e:
return f" 检查失败: {str(e)}"
def _preview_in_browser(self, file_path):
"""在浏览器中预览"""
if not os.path.exists(file_path):
return f" 文件不存在: {file_path}"
try:
# Windows系统
os.startfile(file_path)
return f" 已在默认浏览器中打开: {file_path}"
except Exception as e:
return f" 无法打开浏览器: {str(e)}"
def _analyze_project(self, project_path):
"""分析整个前端项目"""
if not os.path.exists(project_path):
return f" 项目路径不存在: {project_path}"
result = f"\n 前端项目分析: {project_path}\n"
result += "=" * 60 + "\n\n"
try:
# 统计文件
html_files = []
css_files = []
js_files = []
for root, dirs, files in os.walk(project_path):
for file in files:
if file.endswith('.html'):
html_files.append(os.path.join(root, file))
elif file.endswith('.css'):
css_files.append(os.path.join(root, file))
elif file.endswith('.js'):
js_files.append(os.path.join(root, file))
result += f" 项目文件统计:\n"
result += f" - HTML文件: {len(html_files)}\n"
result += f" - CSS文件: {len(css_files)}\n"
result += f" - JavaScript文件: {len(js_files)}\n\n"
# 建议检查
result += f" 建议检查:\n"
if html_files:
result += f" - HTML文件:\n"
for html_file in html_files[:3]: # 只显示前3个
result += f" • {os.path.basename(html_file)}\n"
if len(html_files) > 3:
result += f" • ... 还有 {len(html_files) - 3} 个文件\n"
if css_files:
result += f" - CSS文件:\n"
for css_file in css_files[:3]:
result += f" • {os.path.basename(css_file)}\n"
if len(css_files) > 3:
result += f" • ... 还有 {len(css_files) - 3} 个文件\n"
if js_files:
result += f" - JavaScript文件:\n"
for js_file in js_files[:3]:
result += f" • {os.path.basename(js_file)}\n"
if len(js_files) > 3:
result += f" • ... 还有 {len(js_files) - 3} 个文件\n"
result += f"\n{Fore.GREEN} 分析完成{Style.RESET_ALL}\n"
result += "\n" + "=" * 60 + "\n"
return result
except Exception as e:
return f" 分析失败: {str(e)}"
# 测试函数
def test_plugin():
"""测试插件功能"""
plugin = Plugin()
print("前端测试验证插件测试:")
print("\n1. 创建测试HTML文件:")
test_html = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试页面</title>
</head>
<body>
<h1>测试</h1>
</body>
</html>"""
with open('test.html', 'w', encoding='utf-8') as f:
f.write(test_html)
print("\n2. 验证HTML:")
print(plugin.handle("validate test.html"))
print("\n3. 在浏览器中预览:")
print(plugin.handle("preview test.html"))
print("\n4. 分析项目:")
print(plugin.handle("analyze ."))
# 清理测试文件
if os.path.exists('test.html'):
os.remove('test.html')
if __name__ == "__main__":
test_plugin()