mobile-mcp-ai 2.1.2__py3-none-any.whl → 2.5.8__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.
- mobile_mcp/__init__.py +34 -0
- mobile_mcp/config.py +142 -0
- mobile_mcp/core/basic_tools_lite.py +3266 -0
- {core → mobile_mcp/core}/device_manager.py +2 -2
- mobile_mcp/core/dynamic_config.py +272 -0
- mobile_mcp/core/ios_client_wda.py +569 -0
- mobile_mcp/core/ios_device_manager_wda.py +306 -0
- {core → mobile_mcp/core}/mobile_client.py +279 -39
- mobile_mcp/core/template_matcher.py +429 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {core → mobile_mcp/core}/utils/smart_wait.py +3 -3
- mobile_mcp/mcp_tools/__init__.py +10 -0
- mobile_mcp/mcp_tools/mcp_server.py +1071 -0
- mobile_mcp_ai-2.5.8.dist-info/METADATA +469 -0
- mobile_mcp_ai-2.5.8.dist-info/RECORD +32 -0
- mobile_mcp_ai-2.5.8.dist-info/entry_points.txt +2 -0
- mobile_mcp_ai-2.5.8.dist-info/licenses/LICENSE +201 -0
- mobile_mcp_ai-2.5.8.dist-info/top_level.txt +1 -0
- core/ai/__init__.py +0 -11
- core/ai/ai_analyzer.py +0 -197
- core/ai/ai_config.py +0 -116
- core/ai/ai_platform_adapter.py +0 -399
- core/ai/smart_test_executor.py +0 -520
- core/ai/test_generator.py +0 -365
- core/ai/test_generator_from_history.py +0 -391
- core/ai/test_generator_standalone.py +0 -293
- core/assertion/__init__.py +0 -9
- core/assertion/smart_assertion.py +0 -341
- core/basic_tools.py +0 -377
- core/h5/__init__.py +0 -10
- core/h5/h5_handler.py +0 -548
- core/ios_client.py +0 -219
- core/ios_device_manager.py +0 -252
- core/locator/__init__.py +0 -10
- core/locator/cursor_ai_auto_analyzer.py +0 -119
- core/locator/cursor_vision_helper.py +0 -414
- core/locator/mobile_smart_locator.py +0 -1640
- core/locator/position_analyzer.py +0 -813
- core/locator/script_updater.py +0 -157
- core/nl_test_runner.py +0 -585
- core/smart_app_launcher.py +0 -334
- core/smart_tools.py +0 -311
- mcp/__init__.py +0 -8
- mcp/mcp_server.py +0 -1919
- mcp/mcp_server_simple.py +0 -476
- mobile_mcp_ai-2.1.2.dist-info/METADATA +0 -567
- mobile_mcp_ai-2.1.2.dist-info/RECORD +0 -45
- mobile_mcp_ai-2.1.2.dist-info/entry_points.txt +0 -2
- mobile_mcp_ai-2.1.2.dist-info/top_level.txt +0 -4
- vision/__init__.py +0 -10
- vision/vision_locator.py +0 -404
- {core → mobile_mcp/core}/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/logger.py +0 -0
- {core → mobile_mcp/core}/utils/operation_history_manager.py +0 -0
- {utils → mobile_mcp/utils}/__init__.py +0 -0
- {utils → mobile_mcp/utils}/logger.py +0 -0
- {utils → mobile_mcp/utils}/xml_formatter.py +0 -0
- {utils → mobile_mcp/utils}/xml_parser.py +0 -0
- {mobile_mcp_ai-2.1.2.dist-info → mobile_mcp_ai-2.5.8.dist-info}/WHEEL +0 -0
core/nl_test_runner.py
DELETED
|
@@ -1,585 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
自然语言驱动的测试执行器
|
|
5
|
-
|
|
6
|
-
功能:
|
|
7
|
-
1. 解析自然语言测试步骤
|
|
8
|
-
2. 调用MCP工具执行
|
|
9
|
-
3. Cursor AI分析XML定位元素
|
|
10
|
-
4. 失败时自动视觉识别
|
|
11
|
-
5. 生成Python测试模板
|
|
12
|
-
"""
|
|
13
|
-
import asyncio
|
|
14
|
-
import json
|
|
15
|
-
import re
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import List, Dict, Optional, Any
|
|
18
|
-
from datetime import datetime
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class NLTestRunner:
|
|
22
|
-
"""自然语言测试执行器"""
|
|
23
|
-
|
|
24
|
-
def __init__(self, mcp_tools=None, locator=None, mobile_client=None, script_path=None):
|
|
25
|
-
"""
|
|
26
|
-
初始化自然语言测试执行器
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
mcp_tools: MCP工具字典(mobile_click, mobile_input等)
|
|
30
|
-
locator: MobileSmartLocator实例(用于XML分析)
|
|
31
|
-
mobile_client: MobileClient实例(用于视觉识别)
|
|
32
|
-
script_path: 脚本路径(用于更新用例)
|
|
33
|
-
"""
|
|
34
|
-
self.mcp_tools = mcp_tools or {}
|
|
35
|
-
self.locator = locator
|
|
36
|
-
self.mobile_client = mobile_client
|
|
37
|
-
self.script_path = script_path
|
|
38
|
-
self.execution_log = [] # 执行日志
|
|
39
|
-
self.steps = [] # 解析后的步骤
|
|
40
|
-
self.current_step_index = 0
|
|
41
|
-
|
|
42
|
-
def parse_natural_language(self, nl_text: str) -> List[Dict]:
|
|
43
|
-
"""
|
|
44
|
-
解析自然语言测试步骤
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
nl_text: 自然语言描述,如:
|
|
48
|
-
"启动应用com.im30.way,点击底部第四个图标,点击设置,点击语言,点击English,点击保存"
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
步骤列表
|
|
52
|
-
"""
|
|
53
|
-
steps = []
|
|
54
|
-
|
|
55
|
-
# 简单规则匹配(可以后续用Cursor AI增强)
|
|
56
|
-
# 匹配模式:
|
|
57
|
-
# - 启动应用xxx
|
|
58
|
-
# - 点击xxx
|
|
59
|
-
# - 输入xxx为xxx
|
|
60
|
-
# - 滑动xxx
|
|
61
|
-
# - 等待x秒
|
|
62
|
-
# - 断言xxx
|
|
63
|
-
|
|
64
|
-
# 分割步骤(按逗号、句号、换行)
|
|
65
|
-
parts = re.split(r'[,,。.\n]', nl_text.strip())
|
|
66
|
-
|
|
67
|
-
for part in parts:
|
|
68
|
-
part = part.strip()
|
|
69
|
-
if not part:
|
|
70
|
-
continue
|
|
71
|
-
|
|
72
|
-
# 启动应用
|
|
73
|
-
if '启动应用' in part or '启动' in part or '打开应用' in part or '打开' in part:
|
|
74
|
-
package_match = re.search(r'com\.\w+(?:\.\w+)*', part)
|
|
75
|
-
if package_match:
|
|
76
|
-
steps.append({
|
|
77
|
-
'action': 'launch_app',
|
|
78
|
-
'package': package_match.group(),
|
|
79
|
-
'original_text': part
|
|
80
|
-
})
|
|
81
|
-
continue # 找到后继续下一个
|
|
82
|
-
|
|
83
|
-
# 点击
|
|
84
|
-
elif '点击' in part:
|
|
85
|
-
# 提取元素描述
|
|
86
|
-
element = part.replace('点击', '').strip()
|
|
87
|
-
steps.append({
|
|
88
|
-
'action': 'click',
|
|
89
|
-
'element': element,
|
|
90
|
-
'original_text': part
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
# 输入
|
|
94
|
-
elif '输入' in part:
|
|
95
|
-
# 匹配:输入xxx为xxx 或 输入xxx xxx
|
|
96
|
-
input_match = re.search(r'输入(.+?)(?:为|:|:)(.+)', part)
|
|
97
|
-
if input_match:
|
|
98
|
-
element = input_match.group(1).strip()
|
|
99
|
-
text = input_match.group(2).strip()
|
|
100
|
-
steps.append({
|
|
101
|
-
'action': 'input',
|
|
102
|
-
'element': element,
|
|
103
|
-
'text': text,
|
|
104
|
-
'original_text': part
|
|
105
|
-
})
|
|
106
|
-
else:
|
|
107
|
-
# 简单匹配:输入xxx
|
|
108
|
-
element = part.replace('输入', '').strip()
|
|
109
|
-
steps.append({
|
|
110
|
-
'action': 'input',
|
|
111
|
-
'element': element,
|
|
112
|
-
'text': '', # 需要后续补充
|
|
113
|
-
'original_text': part
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
# 滑动
|
|
117
|
-
elif '滑动' in part:
|
|
118
|
-
direction = None
|
|
119
|
-
if '上' in part or 'up' in part.lower():
|
|
120
|
-
direction = 'up'
|
|
121
|
-
elif '下' in part or 'down' in part.lower():
|
|
122
|
-
direction = 'down'
|
|
123
|
-
elif '左' in part or 'left' in part.lower():
|
|
124
|
-
direction = 'left'
|
|
125
|
-
elif '右' in part or 'right' in part.lower():
|
|
126
|
-
direction = 'right'
|
|
127
|
-
|
|
128
|
-
if direction:
|
|
129
|
-
steps.append({
|
|
130
|
-
'action': 'swipe',
|
|
131
|
-
'direction': direction,
|
|
132
|
-
'original_text': part
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
# 等待
|
|
136
|
-
elif '等待' in part:
|
|
137
|
-
wait_match = re.search(r'(\d+)', part)
|
|
138
|
-
if wait_match:
|
|
139
|
-
seconds = int(wait_match.group(1))
|
|
140
|
-
steps.append({
|
|
141
|
-
'action': 'wait',
|
|
142
|
-
'seconds': seconds,
|
|
143
|
-
'original_text': part
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
# 断言
|
|
147
|
-
elif '断言' in part or '验证' in part or '检查' in part:
|
|
148
|
-
text_match = re.search(r'["\'](.+?)["\']', part)
|
|
149
|
-
if text_match:
|
|
150
|
-
text = text_match.group(1)
|
|
151
|
-
steps.append({
|
|
152
|
-
'action': 'assert_text',
|
|
153
|
-
'text': text,
|
|
154
|
-
'original_text': part
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
self.steps = steps
|
|
158
|
-
return steps
|
|
159
|
-
|
|
160
|
-
async def execute_step(self, step: Dict, step_index: int) -> Dict:
|
|
161
|
-
"""
|
|
162
|
-
执行单个步骤
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
step: 步骤信息
|
|
166
|
-
step_index: 步骤索引
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
执行结果
|
|
170
|
-
"""
|
|
171
|
-
action = step.get('action')
|
|
172
|
-
original_text = step.get('original_text', '')
|
|
173
|
-
|
|
174
|
-
print(f"\n{'='*60}")
|
|
175
|
-
print(f"📋 步骤 {step_index + 1}: {original_text}")
|
|
176
|
-
print(f"{'='*60}")
|
|
177
|
-
|
|
178
|
-
result = {
|
|
179
|
-
'step_index': step_index,
|
|
180
|
-
'action': action,
|
|
181
|
-
'original_text': original_text,
|
|
182
|
-
'success': False,
|
|
183
|
-
'method': None,
|
|
184
|
-
'element_ref': None,
|
|
185
|
-
'coordinate': None,
|
|
186
|
-
'error': None,
|
|
187
|
-
'timestamp': datetime.now().isoformat()
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
try:
|
|
191
|
-
if action == 'launch_app':
|
|
192
|
-
package = step.get('package')
|
|
193
|
-
print(f"🚀 启动应用: {package}")
|
|
194
|
-
if 'mobile_launch_app' in self.mcp_tools:
|
|
195
|
-
await self.mcp_tools['mobile_launch_app'](package_name=package, wait_time=3)
|
|
196
|
-
result['success'] = True
|
|
197
|
-
result['method'] = 'mcp_launch_app'
|
|
198
|
-
else:
|
|
199
|
-
raise ValueError("mobile_launch_app工具不可用")
|
|
200
|
-
|
|
201
|
-
elif action == 'click':
|
|
202
|
-
element = step.get('element')
|
|
203
|
-
print(f"🖱️ 点击: {element}")
|
|
204
|
-
|
|
205
|
-
# 先获取XML
|
|
206
|
-
print(f" 📋 步骤1: 获取页面XML...")
|
|
207
|
-
xml_result = await self._get_xml_snapshot()
|
|
208
|
-
|
|
209
|
-
# Cursor AI分析XML(这里需要调用Cursor AI)
|
|
210
|
-
print(f" 🤖 步骤2: Cursor AI分析XML定位元素...")
|
|
211
|
-
locate_result = await self._cursor_ai_analyze_xml(xml_result, element)
|
|
212
|
-
|
|
213
|
-
if locate_result and locate_result.get('found'):
|
|
214
|
-
# 找到元素,直接使用定位结果执行点击(避免重复定位)
|
|
215
|
-
element_ref = locate_result.get('ref')
|
|
216
|
-
result['element_ref'] = element_ref
|
|
217
|
-
result['method'] = locate_result.get('method', 'xml_analysis')
|
|
218
|
-
|
|
219
|
-
print(f" ✅ 定位成功: {element_ref}")
|
|
220
|
-
print(f" 🖱️ 步骤3: 执行点击...")
|
|
221
|
-
|
|
222
|
-
# 直接使用client点击,避免MCP工具重复定位
|
|
223
|
-
if self.mobile_client:
|
|
224
|
-
try:
|
|
225
|
-
click_result = await self.mobile_client.click(element, ref=element_ref, verify=False)
|
|
226
|
-
if click_result.get('success'):
|
|
227
|
-
result['success'] = True
|
|
228
|
-
else:
|
|
229
|
-
result['error'] = click_result.get('reason', '点击失败')
|
|
230
|
-
except Exception as e:
|
|
231
|
-
result['error'] = f"点击异常: {e}"
|
|
232
|
-
else:
|
|
233
|
-
# 降级:使用MCP工具
|
|
234
|
-
if 'mobile_click' in self.mcp_tools:
|
|
235
|
-
click_result = await self.mcp_tools['mobile_click'](element_desc=element)
|
|
236
|
-
if click_result and click_result.get('success'):
|
|
237
|
-
result['success'] = True
|
|
238
|
-
else:
|
|
239
|
-
result['error'] = click_result.get('error', '点击失败')
|
|
240
|
-
else:
|
|
241
|
-
raise ValueError("mobile_click工具不可用")
|
|
242
|
-
else:
|
|
243
|
-
# XML分析失败,使用视觉识别
|
|
244
|
-
print(f" ⚠️ XML分析失败,使用视觉识别...")
|
|
245
|
-
vision_result = await self._cursor_ai_vision_recognize(element)
|
|
246
|
-
|
|
247
|
-
if vision_result and vision_result.get('coordinate'):
|
|
248
|
-
coord = vision_result['coordinate']
|
|
249
|
-
x, y = coord['x'], coord['y']
|
|
250
|
-
result['coordinate'] = coord
|
|
251
|
-
result['method'] = 'vision_recognition'
|
|
252
|
-
|
|
253
|
-
print(f" ✅ 视觉识别成功: ({x}, {y})")
|
|
254
|
-
print(f" 🖱️ 步骤3: 使用坐标点击...")
|
|
255
|
-
|
|
256
|
-
# 直接使用坐标点击(不通过MCP工具,避免重复定位)
|
|
257
|
-
if self.mobile_client:
|
|
258
|
-
try:
|
|
259
|
-
self.mobile_client.u2.click(x, y)
|
|
260
|
-
result['success'] = True
|
|
261
|
-
print(f" ✅ 坐标点击成功")
|
|
262
|
-
except Exception as e:
|
|
263
|
-
result['error'] = f"坐标点击失败: {e}"
|
|
264
|
-
else:
|
|
265
|
-
result['error'] = "mobile_client不可用,无法执行坐标点击"
|
|
266
|
-
else:
|
|
267
|
-
result['error'] = "无法定位元素(XML分析和视觉识别都失败)"
|
|
268
|
-
|
|
269
|
-
elif action == 'input':
|
|
270
|
-
element = step.get('element')
|
|
271
|
-
text = step.get('text')
|
|
272
|
-
print(f"⌨️ 输入: {element} = {text}")
|
|
273
|
-
|
|
274
|
-
# 获取XML并分析
|
|
275
|
-
xml_result = await self._get_xml_snapshot()
|
|
276
|
-
locate_result = await self._cursor_ai_analyze_xml(xml_result, element)
|
|
277
|
-
|
|
278
|
-
if locate_result and locate_result.get('found'):
|
|
279
|
-
element_ref = locate_result.get('ref')
|
|
280
|
-
result['element_ref'] = element_ref
|
|
281
|
-
result['method'] = locate_result.get('method', 'xml_analysis')
|
|
282
|
-
|
|
283
|
-
if 'mobile_input' in self.mcp_tools:
|
|
284
|
-
await self.mcp_tools['mobile_input'](element_desc=element, text=text)
|
|
285
|
-
result['success'] = True
|
|
286
|
-
else:
|
|
287
|
-
raise ValueError("mobile_input工具不可用")
|
|
288
|
-
else:
|
|
289
|
-
result['error'] = "无法定位输入框"
|
|
290
|
-
|
|
291
|
-
elif action == 'swipe':
|
|
292
|
-
direction = step.get('direction')
|
|
293
|
-
print(f"👆 滑动: {direction}")
|
|
294
|
-
|
|
295
|
-
if 'mobile_swipe' in self.mcp_tools:
|
|
296
|
-
swipe_result = await self.mcp_tools['mobile_swipe'](direction=direction)
|
|
297
|
-
if swipe_result and swipe_result.get('success'):
|
|
298
|
-
result['success'] = True
|
|
299
|
-
else:
|
|
300
|
-
result['error'] = swipe_result.get('error', '滑动失败')
|
|
301
|
-
else:
|
|
302
|
-
raise ValueError("mobile_swipe工具不可用")
|
|
303
|
-
|
|
304
|
-
elif action == 'wait':
|
|
305
|
-
seconds = step.get('seconds', 1)
|
|
306
|
-
print(f"⏳ 等待: {seconds}秒")
|
|
307
|
-
await asyncio.sleep(seconds)
|
|
308
|
-
result['success'] = True
|
|
309
|
-
|
|
310
|
-
elif action == 'assert_text':
|
|
311
|
-
text = step.get('text')
|
|
312
|
-
print(f"✅ 断言: 检查文本 '{text}'")
|
|
313
|
-
|
|
314
|
-
if 'mobile_assert_text' in self.mcp_tools:
|
|
315
|
-
assert_result = await self.mcp_tools['mobile_assert_text'](text=text)
|
|
316
|
-
if assert_result and assert_result.get('found'):
|
|
317
|
-
result['success'] = True
|
|
318
|
-
else:
|
|
319
|
-
result['error'] = f"未找到文本: {text}"
|
|
320
|
-
else:
|
|
321
|
-
raise ValueError("mobile_assert_text工具不可用")
|
|
322
|
-
|
|
323
|
-
except Exception as e:
|
|
324
|
-
result['error'] = str(e)
|
|
325
|
-
print(f" ❌ 执行失败: {e}")
|
|
326
|
-
import traceback
|
|
327
|
-
traceback.print_exc()
|
|
328
|
-
|
|
329
|
-
# 记录执行日志
|
|
330
|
-
self.execution_log.append(result)
|
|
331
|
-
|
|
332
|
-
if result['success']:
|
|
333
|
-
print(f" ✅ 步骤执行成功")
|
|
334
|
-
else:
|
|
335
|
-
print(f" ❌ 步骤执行失败: {result.get('error')}")
|
|
336
|
-
|
|
337
|
-
return result
|
|
338
|
-
|
|
339
|
-
async def _get_xml_snapshot(self) -> Dict:
|
|
340
|
-
"""获取XML快照"""
|
|
341
|
-
if 'mobile_snapshot' in self.mcp_tools:
|
|
342
|
-
snapshot_result = await self.mcp_tools['mobile_snapshot']()
|
|
343
|
-
if snapshot_result and snapshot_result.get('success'):
|
|
344
|
-
return {
|
|
345
|
-
'xml': snapshot_result.get('snapshot', ''),
|
|
346
|
-
'success': True
|
|
347
|
-
}
|
|
348
|
-
return {'success': False, 'xml': ''}
|
|
349
|
-
|
|
350
|
-
async def _cursor_ai_analyze_xml(self, xml_result: Dict, element_desc: str) -> Optional[Dict]:
|
|
351
|
-
"""
|
|
352
|
-
Cursor AI分析XML定位元素
|
|
353
|
-
|
|
354
|
-
使用现有的MobileSmartLocator进行定位
|
|
355
|
-
"""
|
|
356
|
-
if not xml_result.get('success'):
|
|
357
|
-
return None
|
|
358
|
-
|
|
359
|
-
# 🎯 使用现有的locator进行定位
|
|
360
|
-
# 注意:这里需要传入locator实例
|
|
361
|
-
if hasattr(self, 'locator') and self.locator:
|
|
362
|
-
try:
|
|
363
|
-
result = await self.locator.locate(element_desc)
|
|
364
|
-
if result:
|
|
365
|
-
return {
|
|
366
|
-
'found': True,
|
|
367
|
-
'ref': result.get('ref', ''),
|
|
368
|
-
'method': result.get('method', 'xml_analysis'),
|
|
369
|
-
'confidence': result.get('confidence', 80)
|
|
370
|
-
}
|
|
371
|
-
except Exception as e:
|
|
372
|
-
print(f" ⚠️ XML分析异常: {e}")
|
|
373
|
-
|
|
374
|
-
return None
|
|
375
|
-
|
|
376
|
-
async def _cursor_ai_vision_recognize(self, element_desc: str) -> Optional[Dict]:
|
|
377
|
-
"""
|
|
378
|
-
Cursor AI视觉识别
|
|
379
|
-
|
|
380
|
-
使用现有的视觉识别功能
|
|
381
|
-
"""
|
|
382
|
-
try:
|
|
383
|
-
from mobile_mcp.core.locator.cursor_vision_helper import CursorVisionHelper
|
|
384
|
-
|
|
385
|
-
if hasattr(self, 'mobile_client') and self.mobile_client:
|
|
386
|
-
cursor_helper = CursorVisionHelper(self.mobile_client)
|
|
387
|
-
script_path = getattr(self, 'script_path', None)
|
|
388
|
-
result = await cursor_helper.analyze_with_cursor(
|
|
389
|
-
element_desc,
|
|
390
|
-
script_path=script_path,
|
|
391
|
-
auto_analyze=True
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
if result and result.get('status') == 'completed':
|
|
395
|
-
coord = result.get('coordinate')
|
|
396
|
-
if coord:
|
|
397
|
-
return {
|
|
398
|
-
'coordinate': coord,
|
|
399
|
-
'screenshot_path': result.get('screenshot_path'),
|
|
400
|
-
'confidence': coord.get('confidence', 80)
|
|
401
|
-
}
|
|
402
|
-
except Exception as e:
|
|
403
|
-
print(f" ⚠️ 视觉识别异常: {e}")
|
|
404
|
-
import traceback
|
|
405
|
-
traceback.print_exc()
|
|
406
|
-
|
|
407
|
-
return None
|
|
408
|
-
|
|
409
|
-
async def execute(self, nl_text: str) -> Dict:
|
|
410
|
-
"""
|
|
411
|
-
执行自然语言测试
|
|
412
|
-
|
|
413
|
-
Args:
|
|
414
|
-
nl_text: 自然语言测试描述
|
|
415
|
-
|
|
416
|
-
Returns:
|
|
417
|
-
执行结果
|
|
418
|
-
"""
|
|
419
|
-
print("=" * 60)
|
|
420
|
-
print("🚀 自然语言测试执行器")
|
|
421
|
-
print("=" * 60)
|
|
422
|
-
print(f"\n📝 输入: {nl_text}\n")
|
|
423
|
-
|
|
424
|
-
# 解析自然语言
|
|
425
|
-
steps = self.parse_natural_language(nl_text)
|
|
426
|
-
print(f"✅ 解析完成,共 {len(steps)} 个步骤\n")
|
|
427
|
-
|
|
428
|
-
# 执行步骤
|
|
429
|
-
success_count = 0
|
|
430
|
-
fail_count = 0
|
|
431
|
-
|
|
432
|
-
for i, step in enumerate(steps):
|
|
433
|
-
result = await self.execute_step(step, i)
|
|
434
|
-
|
|
435
|
-
if result['success']:
|
|
436
|
-
success_count += 1
|
|
437
|
-
else:
|
|
438
|
-
fail_count += 1
|
|
439
|
-
# 可以选择是否继续执行
|
|
440
|
-
# break
|
|
441
|
-
|
|
442
|
-
# 生成Python模板
|
|
443
|
-
python_code = self.generate_python_template()
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
'total_steps': len(steps),
|
|
447
|
-
'success_count': success_count,
|
|
448
|
-
'fail_count': fail_count,
|
|
449
|
-
'execution_log': self.execution_log,
|
|
450
|
-
'python_template': python_code
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
def generate_python_template(self) -> str:
|
|
454
|
-
"""
|
|
455
|
-
生成Python测试模板
|
|
456
|
-
|
|
457
|
-
基于执行日志生成可复用的测试代码
|
|
458
|
-
"""
|
|
459
|
-
lines = [
|
|
460
|
-
"#!/usr/bin/env python3",
|
|
461
|
-
"# -*- coding: utf-8 -*-",
|
|
462
|
-
'"""',
|
|
463
|
-
"自动生成的测试用例",
|
|
464
|
-
f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
465
|
-
'"""',
|
|
466
|
-
"import asyncio",
|
|
467
|
-
"import sys",
|
|
468
|
-
"from pathlib import Path",
|
|
469
|
-
"",
|
|
470
|
-
"sys.path.insert(0, str(Path(__file__).parent.parent.parent))",
|
|
471
|
-
"from mobile_mcp.core.mobile_client import MobileClient",
|
|
472
|
-
"from mobile_mcp.core.locator.mobile_smart_locator import MobileSmartLocator",
|
|
473
|
-
"",
|
|
474
|
-
"",
|
|
475
|
-
"async def main():",
|
|
476
|
-
" client = MobileClient()",
|
|
477
|
-
" locator = MobileSmartLocator(client)",
|
|
478
|
-
"",
|
|
479
|
-
]
|
|
480
|
-
|
|
481
|
-
# 添加步骤
|
|
482
|
-
for i, log in enumerate(self.execution_log):
|
|
483
|
-
action = log['action']
|
|
484
|
-
original_text = log.get('original_text', '')
|
|
485
|
-
|
|
486
|
-
if action == 'launch_app':
|
|
487
|
-
package = log.get('package', '')
|
|
488
|
-
lines.append(f" # 步骤 {i+1}: {original_text}")
|
|
489
|
-
lines.append(f" await client.launch_app('{package}', wait_time=3)")
|
|
490
|
-
lines.append(f" await asyncio.sleep(1)")
|
|
491
|
-
lines.append("")
|
|
492
|
-
|
|
493
|
-
elif action == 'click':
|
|
494
|
-
element = log.get('element', '')
|
|
495
|
-
method = log.get('method', '')
|
|
496
|
-
element_ref = log.get('element_ref')
|
|
497
|
-
coordinate = log.get('coordinate')
|
|
498
|
-
|
|
499
|
-
lines.append(f" # 步骤 {i+1}: {original_text}")
|
|
500
|
-
lines.append(f" # 定位方法: {method}")
|
|
501
|
-
|
|
502
|
-
if coordinate:
|
|
503
|
-
x, y = coordinate['x'], coordinate['y']
|
|
504
|
-
lines.append(f" # Cursor AI坐标: ({x}, {y})")
|
|
505
|
-
lines.append(f" client.u2.click({x}, {y})")
|
|
506
|
-
elif element_ref:
|
|
507
|
-
lines.append(f" result = await locator.locate('{element}')")
|
|
508
|
-
lines.append(f" if result:")
|
|
509
|
-
lines.append(f" await client.click('{element}', ref=result['ref'])")
|
|
510
|
-
else:
|
|
511
|
-
lines.append(f" result = await locator.locate('{element}')")
|
|
512
|
-
lines.append(f" if result:")
|
|
513
|
-
lines.append(f" await client.click('{element}', ref=result['ref'])")
|
|
514
|
-
|
|
515
|
-
lines.append(f" await asyncio.sleep(0.5)")
|
|
516
|
-
lines.append("")
|
|
517
|
-
|
|
518
|
-
elif action == 'input':
|
|
519
|
-
element = log.get('element', '')
|
|
520
|
-
text = log.get('text', '')
|
|
521
|
-
lines.append(f" # 步骤 {i+1}: {original_text}")
|
|
522
|
-
lines.append(f" result = await locator.locate('{element}')")
|
|
523
|
-
lines.append(f" if result:")
|
|
524
|
-
lines.append(f" await client.type_text('{element}', '{text}', ref=result['ref'])")
|
|
525
|
-
lines.append("")
|
|
526
|
-
|
|
527
|
-
elif action == 'swipe':
|
|
528
|
-
direction = log.get('direction', '')
|
|
529
|
-
lines.append(f" # 步骤 {i+1}: {original_text}")
|
|
530
|
-
lines.append(f" await client.swipe('{direction}')")
|
|
531
|
-
lines.append("")
|
|
532
|
-
|
|
533
|
-
elif action == 'wait':
|
|
534
|
-
seconds = log.get('seconds', 1)
|
|
535
|
-
lines.append(f" # 步骤 {i+1}: {original_text}")
|
|
536
|
-
lines.append(f" await asyncio.sleep({seconds})")
|
|
537
|
-
lines.append("")
|
|
538
|
-
|
|
539
|
-
elif action == 'assert_text':
|
|
540
|
-
text = log.get('text', '')
|
|
541
|
-
lines.append(f" # 步骤 {i+1}: {original_text}")
|
|
542
|
-
lines.append(f" snapshot = await client.snapshot()")
|
|
543
|
-
lines.append(f" assert '{text}' in snapshot, f\"未找到文本: {text}\"")
|
|
544
|
-
lines.append("")
|
|
545
|
-
|
|
546
|
-
lines.extend([
|
|
547
|
-
" client.device_manager.disconnect()",
|
|
548
|
-
"",
|
|
549
|
-
"",
|
|
550
|
-
"if __name__ == \"__main__\":",
|
|
551
|
-
" asyncio.run(main())",
|
|
552
|
-
])
|
|
553
|
-
|
|
554
|
-
return '\n'.join(lines)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
async def main():
|
|
558
|
-
"""测试主函数"""
|
|
559
|
-
# 这里需要传入MCP工具
|
|
560
|
-
# 实际使用时,应该从MCP Server获取工具
|
|
561
|
-
|
|
562
|
-
runner = NLTestRunner()
|
|
563
|
-
|
|
564
|
-
# 测试自然语言
|
|
565
|
-
nl_text = """
|
|
566
|
-
启动应用com.im30.way,点击底部第四个图标,点击设置,点击语言,点击English,点击保存
|
|
567
|
-
"""
|
|
568
|
-
|
|
569
|
-
result = await runner.execute(nl_text)
|
|
570
|
-
|
|
571
|
-
print("\n" + "=" * 60)
|
|
572
|
-
print("📊 执行总结")
|
|
573
|
-
print("=" * 60)
|
|
574
|
-
print(f"总步骤数: {result['total_steps']}")
|
|
575
|
-
print(f"成功: {result['success_count']}")
|
|
576
|
-
print(f"失败: {result['fail_count']}")
|
|
577
|
-
print("\n" + "=" * 60)
|
|
578
|
-
print("📝 生成的Python模板:")
|
|
579
|
-
print("=" * 60)
|
|
580
|
-
print(result['python_template'])
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if __name__ == "__main__":
|
|
584
|
-
asyncio.run(main())
|
|
585
|
-
|