Skip to content

Commit 49e21fe

Browse files
committed
feat: Flutter移动端测试全流程修复 - 24/24通过
核心改动: - input_text_xy: 去掉Appium findElement(Flutter上超时), 改为纯adb方案(tap+input text) - dump_ui_tree: Appium /source优先, adb dump fallback(两者互补) - _analyze_page_elements: UI树+AI视觉双保险预加载 - _get_text_from_ui_tree: assert_text用UI树快速路径 - navigate后清空scene_coords并重新分析剩余步骤 - EditText叶子节点去重(Flutter父子两层结构) - 清空文本改为3批x5个DEL(避免单条命令超时) - .gitignore添加调试临时文件
1 parent 64a9934 commit 49e21fe

12 files changed

Lines changed: 1046 additions & 178 deletions

File tree

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,14 @@ desktop/dist/
5959

6060
# Tauri (Rust桌面应用)
6161
desktop/src-tauri/target/
62+
63+
# 调试临时文件
64+
diag*.py
65+
diag*.txt
66+
_diag*.py
67+
_dump*.py
68+
dump*.xml
69+
ui_dump*.xml
70+
ui_tree_debug*.xml
71+
flutter_*_source.xml
72+
test_sid.txt

.windsurfrules

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 重要工作流程规则
2+
3+
## 请在结束回复前运行弹窗命令
4+
5+
在结束回复之前,请运行以下弹窗命令:
6+
node "C:/Users/37561/.niuma-mcp/dialog-trigger.js" --port 13706 "AI想要结束的原因摘要"
7+
8+
## 输出长度提醒
9+
10+
当你预计输出内容较长时,请**提前运行弹窗命令**。
11+
- 每完成一个独立步骤后运行弹窗
12+
- 多运行几次弹窗比输出被截断要好
13+
14+
## 执行流程
15+
16+
1. 完成任务后运行上述命令
17+
2. 命令运行后,检查终端输出中的 ACTION 值
18+
3. **请读取响应文件获取完整用户指令**:
19+
20+
read_file "C:/Users/37561/.niuma-mcp/last_response.md"
21+
22+
4. 根据响应文件执行用户指令
23+
5. 完成后再次运行弹窗命令确认
24+
25+
## 响应文件规则
26+
27+
弹窗命令运行后,完整的用户反馈会保存到 `C:/Users/37561/.niuma-mcp/last_response.md`。
28+
29+
**请使用 read_file 读取此文件!**
30+
31+
- 终端输出只是预览,可能会被截断
32+
- 响应文件包含完整的用户指令
33+
34+
## 核心准则
35+
36+
1. 完整输出 - 不要省略代码
37+
2. 分步执行 - 将大任务拆分为多轮弹窗
38+
3. 弹窗确认 - 请在结束前运行弹窗命令
39+
4. 读取响应文件 - 获取完整用户指令
40+
5. 执行用户指令 - 按照用户要求执行
41+
42+
## 结束条件
43+
44+
请仅在响应文件中 ACTION 为 end 或 stop 时结束。
45+
否则请再次运行弹窗命令。

android-demo/testpilot/testpilot.json

Lines changed: 0 additions & 37 deletions
This file was deleted.

extension/src/engineClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface TestReportResponse {
5454
bugs: BugDetail[];
5555
repair_summary: string | null;
5656
fixed_bug_count: number | null;
57+
stopped?: boolean;
5758
}
5859

5960
/** WebSocket 消息类型 */

extension/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ export function activate(context: vscode.ExtensionContext): void {
458458
let appiumOk = await httpCheck("http://127.0.0.1:4723/status");
459459
if (!appiumOk) {
460460
progress.report({ message: "Appium 未运行,正在启动..." });
461-
spawn("appium", ["--port", "4723"], { detached: true, stdio: "ignore" }).unref();
461+
spawn("appium", ["--port", "4723"], { detached: true, stdio: "ignore", shell: true, windowsHide: true }).unref();
462462
for (let i = 0; i < 12; i++) {
463463
await new Promise((r) => setTimeout(r, 1000));
464464
if (await httpCheck("http://127.0.0.1:4723/status")) { appiumOk = true; break; }

extension/src/sidebarProvider.ts

Lines changed: 42 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
205205
try {
206206
this._postMessage({ command: "testStarted" });
207207

208-
// 依次执行每个蓝本,汇总结果
208+
// 依次执行每个蓝本,汇总结果(用户停止时中断后续蓝本)
209209
const results: TestReportResponse[] = [];
210+
let userStopped = false;
210211
for (const bp of msg.blueprint_paths) {
212+
if (userStopped) { break; }
211213
try {
212214
const platform = (msg.platform || "web").toLowerCase();
213215
let report: TestReportResponse;
@@ -237,6 +239,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
237239
}
238240

239241
results.push(report);
242+
243+
// 检查是否被用户停止,停止则中断后续蓝本
244+
if (report.stopped) {
245+
userStopped = true;
246+
}
240247
} catch (err: unknown) {
241248
const errMsg = err instanceof Error ? err.message : String(err);
242249
results.push({
@@ -262,7 +269,10 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
262269
const passRate = totalSteps > 0 ? (passedSteps / totalSteps * 100) : 0;
263270

264271
let md = `# 批量蓝本测试汇总\n\n`;
265-
md += `- 蓝本数: ${results.length}\n`;
272+
if (userStopped) {
273+
md += `> ⚠️ 用户手动停止,已执行 ${results.length}/${msg.blueprint_paths.length} 个蓝本\n\n`;
274+
}
275+
md += `- 蓝本数: ${results.length}${userStopped ? `/${msg.blueprint_paths.length}` : ""}\n`;
266276
md += `- 总步骤: ${totalSteps}(通过 ${passedSteps} / 失败 ${failedSteps})\n`;
267277
md += `- 总Bug数: ${totalBugs}\n`;
268278
md += `- 总通过率: ${passRate.toFixed(0)}%\n`;
@@ -724,6 +734,7 @@ ${commonRules}`;
724734
const platform = (msg.platform || "web").toLowerCase();
725735
try {
726736
if (platform === "android" || platform === "ios") {
737+
// 只检查设备是否连接,Session 由引擎在蓝本测试时自动创建
727738
const devices = await this._client.listMobileDevices();
728739
if ((devices.count || 0) === 0) {
729740
this._postMessage({
@@ -737,31 +748,14 @@ ${commonRules}`;
737748
return;
738749
}
739750

740-
const sessions = await this._client.listMobileSessions();
741-
if ((sessions.count || 0) === 0) {
742-
const first = devices.devices?.[0] || {};
743-
const deviceName = String((first as Record<string, unknown>).model || (first as Record<string, unknown>).serial || "");
744-
const created = await this._client.createMobileSession({ device_name: deviceName });
745-
this._postMessage({
746-
command: "platformPrecheckResult",
747-
data: {
748-
ok: true,
749-
platform,
750-
message: `设备已连接,已创建会话 ${created.session_id},可以开始测试。`,
751-
mobile_session_id: created.session_id,
752-
},
753-
});
754-
return;
755-
}
756-
757-
const sid = String((sessions.sessions?.[0] as Record<string, unknown>)?.session_id || "");
751+
const first = devices.devices?.[0] || {};
752+
const model = String((first as Record<string, unknown>).model || (first as Record<string, unknown>).serial || "设备");
758753
this._postMessage({
759754
command: "platformPrecheckResult",
760755
data: {
761756
ok: true,
762757
platform,
763-
message: `检测到已连接设备和活跃会话 ${sid},可以开始测试。`,
764-
mobile_session_id: sid,
758+
message: `设备 ${model} 已连接,Appium Session 将自动创建。`,
765759
},
766760
});
767761
return;
@@ -814,7 +808,7 @@ ${commonRules}`;
814808
}
815809

816810
private async _handleConnectDevice(_msg: { platform?: string }): Promise<void> {
817-
const { exec } = require("child_process") as typeof import("child_process");
811+
const { exec, spawn } = require("child_process") as typeof import("child_process");
818812
const run = (cmd: string): Promise<string> =>
819813
new Promise((resolve) => {
820814
exec(cmd, { timeout: 10000 }, (_err, stdout) => resolve(stdout || ""));
@@ -852,11 +846,29 @@ ${commonRules}`;
852846
fail(`设备 ${model} 未安装 Appium 自动化组件(uiautomator2),首次测试时会自动安装。\n也可手动运行:appium driver install uiautomator2`);
853847
return;
854848
}
855-
// 4. 检查 Appium server 是否运行
856-
const appiumOk = await httpCheck("http://127.0.0.1:4723/status");
849+
// 4. 检查 Appium server 是否运行,未运行则自动启动
850+
let appiumOk = await httpCheck("http://127.0.0.1:4723/status");
857851
if (!appiumOk) {
858-
fail(`设备 ${model} 就绪,但 Appium 未运行。\n请先在终端运行:appium\n或点击"启动引擎"按钮`);
859-
return;
852+
// 自动启动 Appium
853+
this._postMessage({
854+
command: "connectDeviceResult",
855+
data: { ok: false, message: `设备 ${model} 就绪,Appium 未运行,正在自动启动...` },
856+
});
857+
try {
858+
spawn("appium", ["--port", "4723"], { detached: true, stdio: "ignore", shell: true, windowsHide: true }).unref();
859+
} catch (spawnErr: unknown) {
860+
fail(`Appium 启动失败: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}\n请手动运行:appium --port 4723`);
861+
return;
862+
}
863+
// 等待 Appium 启动(最多12秒)
864+
for (let i = 0; i < 12; i++) {
865+
await new Promise<void>((r) => setTimeout(r, 1000));
866+
if (await httpCheck("http://127.0.0.1:4723/status")) { appiumOk = true; break; }
867+
}
868+
if (!appiumOk) {
869+
fail(`设备 ${model} 就绪,但 Appium 启动超时(12秒)。\n请手动运行:appium --port 4723`);
870+
return;
871+
}
860872
}
861873
// 5. 检查引擎
862874
const engineOk = await httpCheck("http://127.0.0.1:8900/health");
@@ -1062,11 +1074,6 @@ ${commonRules}`;
10621074
</div>
10631075
<div style="display:flex;gap:4px;margin-top:2px">
10641076
<button id="btnDetectDevice" class="btn-secondary" style="font-size:10px;padding:3px 6px;flex:1">检测设备</button>
1065-
<button id="btnHandshake" class="btn-secondary hidden" style="font-size:10px;padding:3px 6px;flex:1">🤝 握手</button>
1066-
</div>
1067-
<div id="handshakeStatusRow" class="hidden" style="display:flex;align-items:center;gap:4px;margin-top:4px;font-size:10px">
1068-
<span id="handshakeIcon">⏳</span>
1069-
<span id="handshakeText" style="color:var(--muted);word-break:break-word">未握手</span>
10701077
</div>
10711078
</div>
10721079
<button id="btnLaunchEngine" class="hidden" style="background:#22c55e;margin-top:6px">🚀 一键启动引擎</button>
@@ -1273,7 +1280,6 @@ ${commonRules}`;
12731280
} else if (platform === "android") {
12741281
deviceRow.classList.remove("hidden");
12751282
document.getElementById("btnDetectDevice").style.display = "";
1276-
document.getElementById("btnHandshake").classList.remove("hidden");
12771283
checkDeviceStatus();
12781284
} else {
12791285
deviceRow.classList.add("hidden");
@@ -1295,21 +1301,6 @@ ${commonRules}`;
12951301
checkDeviceStatus();
12961302
});
12971303
1298-
// 握手按钮
1299-
document.getElementById("btnHandshake").addEventListener("click", () => {
1300-
const btn = document.getElementById("btnHandshake");
1301-
const row = document.getElementById("handshakeStatusRow");
1302-
const icon = document.getElementById("handshakeIcon");
1303-
const text = document.getElementById("handshakeText");
1304-
btn.disabled = true;
1305-
btn.textContent = "⏳ 握手中...";
1306-
row.classList.remove("hidden");
1307-
icon.textContent = "⏳";
1308-
text.textContent = "正在检测 Appium 环境...";
1309-
text.style.color = "var(--muted)";
1310-
vscode.postMessage({ command: "connectDevice", platform: getCurrentPlatform() });
1311-
});
1312-
13131304
// 刷新项目按钮
13141305
document.getElementById("btnRefreshProjects").addEventListener("click", () => {
13151306
vscode.postMessage({ command: "scanBlueprints" });
@@ -1440,7 +1431,6 @@ ${commonRules}`;
14401431
addLog("正在断开引擎...", "info");
14411432
});
14421433
1443-
let currentMobileSessionId = "";
14441434
let pendingBlueprintRun = null;
14451435
14461436
// 蓝本测试(支持多选批量 + 平台路由 + 前置检查)
@@ -1508,45 +1498,21 @@ ${commonRules}`;
15081498
case "blueprintSelected": onBlueprintSelected(msg.data); break;
15091499
case "platformPrecheckResult": onPlatformPrecheckResult(msg.data); break;
15101500
case "deviceStatusResult": onDeviceStatusResult(msg.data); break;
1511-
case "connectDeviceResult": onConnectDeviceResult(msg.data); break;
15121501
}
15131502
});
15141503
15151504
function onDeviceStatusResult(data) {
15161505
const statusText = document.getElementById("deviceStatusText");
15171506
const statusIcon = document.getElementById("deviceStatusIcon");
1518-
const btnHandshake = document.getElementById("btnHandshake");
15191507
if (!data) return;
15201508
if (data.connected) {
1521-
statusText.textContent = data.message || "设备已连接";
1509+
statusText.textContent = (data.message || "设备已连接") + "(运行蓝本时自动连接)";
15221510
statusText.style.color = "var(--success,#22c55e)";
15231511
statusIcon.textContent = "✅";
1524-
btnHandshake.classList.remove("hidden");
15251512
} else {
15261513
statusText.textContent = data.message || "未检测到设备";
15271514
statusText.style.color = "var(--error,#ef4444)";
15281515
statusIcon.textContent = "❌";
1529-
btnHandshake.classList.add("hidden");
1530-
document.getElementById("handshakeStatusRow").classList.add("hidden");
1531-
}
1532-
}
1533-
1534-
function onConnectDeviceResult(data) {
1535-
const row = document.getElementById("handshakeStatusRow");
1536-
const icon = document.getElementById("handshakeIcon");
1537-
const text = document.getElementById("handshakeText");
1538-
const btn = document.getElementById("btnHandshake");
1539-
row.classList.remove("hidden");
1540-
btn.disabled = false;
1541-
btn.textContent = "🤝 握手";
1542-
if (data.ok) {
1543-
icon.textContent = "✅";
1544-
text.textContent = data.message || "握手成功";
1545-
text.style.color = "var(--success,#22c55e)";
1546-
} else {
1547-
icon.textContent = "❌";
1548-
text.textContent = data.message || "握手失败";
1549-
text.style.color = "var(--error,#ef4444)";
15501516
}
15511517
}
15521518
@@ -1560,10 +1526,6 @@ ${commonRules}`;
15601526
return;
15611527
}
15621528
1563-
if (data.mobile_session_id) {
1564-
currentMobileSessionId = data.mobile_session_id;
1565-
}
1566-
15671529
addLog(data.message || "平台检查通过", "success");
15681530
15691531
const run = pendingBlueprintRun;
@@ -1577,15 +1539,13 @@ ${commonRules}`;
15771539
blueprint_paths: run.paths,
15781540
base_url: run.baseUrl,
15791541
platform: run.platform,
1580-
mobile_session_id: currentMobileSessionId || undefined,
15811542
});
15821543
} else {
15831544
vscode.postMessage({
15841545
command: "blueprintTest",
15851546
blueprint_path: run.paths[0],
15861547
base_url: run.baseUrl,
15871548
platform: run.platform,
1588-
mobile_session_id: currentMobileSessionId || undefined,
15891549
});
15901550
}
15911551
}
@@ -1826,18 +1786,9 @@ ${commonRules}`;
18261786
stepList.innerHTML = "";
18271787
stepSection.classList.add("hidden");
18281788
logArea.innerHTML = "";
1829-
addLog("测试任务已启动...", "info");
1830-
// 持续跳动提示,让用户知道系统在运行
1831-
let dots = 0;
1832-
const phases = ["正在连接模拟器...", "正在执行测试步骤...", "仍在测试中,请耐心等待..."];
1833-
let phase = 0;
1789+
addLog("测试任务已启动,步骤进度将实时显示...", "info");
18341790
if (testingTimer) clearInterval(testingTimer);
1835-
testingTimer = setInterval(() => {
1836-
dots = (dots + 1) % 4;
1837-
const dotStr = ".".repeat(dots + 1);
1838-
addLog("⏳ " + phases[phase] + dotStr, "info");
1839-
if (dots === 3) phase = Math.min(phase + 1, phases.length - 1);
1840-
}, 5000);
1791+
testingTimer = null;
18411792
}
18421793
18431794
function onTestResult(report) {

0 commit comments

Comments
 (0)