Skip to content

Commit 802b127

Browse files
committed
v10.2: start_command自动启动应用+预览服务器适配testpilot/目录+提示词更新(947 passed)
1 parent c0c9d78 commit 802b127

5 files changed

Lines changed: 78 additions & 5 deletions

File tree

extension/src/sidebarProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,12 +359,13 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
359359
}
360360

361361
private async _handleCopyBlueprintPrompt(): Promise<void> {
362-
const prompt = `请帮我为当前项目生成 testpilot.json 测试蓝本文件,放在项目根目录。要求:
362+
const prompt = `请帮我为当前项目生成 testpilot.json 测试蓝本文件,放在项目的 testpilot/ 文件夹下。要求:
363363
1. 分析源代码中所有可操作 UI 元素(按钮/表单/导航/弹窗)
364364
2. 选择器使用代码中的真实 id(如 #login-btn)或稳定 class,禁止用 div:nth-child(3) 这类脆弱选择器
365365
3. 每个功能页面对应一个场景,覆盖正常流程和异常场景(空表单提交、错误输入)
366366
4. 每个 fill 操作后必须有 assert_text 或 screenshot 验证
367367
5. 每次 navigate 必须有断言验证页面已正确加载
368+
6. 如果应用需要命令行启动(如 npm start、python app.py),必须填写 start_command 字段;纯HTML静态应用留空即可
368369
369370
格式:
370371
{
@@ -373,6 +374,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
373374
"base_url": "http://localhost:端口",
374375
"version": "1.0",
375376
"platform": "web",
377+
"start_command": "npm start 或 python app.py(纯HTML留空)",
378+
"start_cwd": "./(启动命令的工作目录,默认项目根目录)",
376379
"pages": [
377380
{
378381
"url": "/",

src/api/routes.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,18 @@ async def list_preview_apps() -> list[dict]:
151151
root = Path(__file__).resolve().parent.parent.parent
152152
apps = []
153153
for child in sorted(root.iterdir()):
154+
if not child.is_dir():
155+
continue
154156
bp_file = child / "testpilot.json"
155-
if child.is_dir() and bp_file.exists():
157+
tp_dir = child / "testpilot"
158+
has_bp = bp_file.exists()
159+
if not has_bp and tp_dir.is_dir():
160+
has_bp = any(tp_dir.glob("*.json"))
161+
if has_bp:
156162
apps.append({
157163
"name": child.name,
158164
"path": str(child),
159-
"blueprint": str(bp_file),
165+
"blueprint_dir": str(tp_dir) if tp_dir.is_dir() else str(bp_file.parent),
160166
"preview_url": f"http://localhost:{port}/preview/{child.name}/",
161167
})
162168
return apps

src/app.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,15 @@ async def websocket_endpoint(ws: WebSocket) -> None:
113113
project_root = Path(__file__).resolve().parent.parent
114114
_preview_dirs: dict[str, Path] = {}
115115

116-
# 扫描第一层子目录中的 testpilot.json
116+
# 扫描第一层子目录中含蓝本的目录(支持 testpilot.json 或 testpilot/ 子目录)
117117
for child in project_root.iterdir():
118-
if child.is_dir() and (child / "testpilot.json").exists():
118+
if not child.is_dir():
119+
continue
120+
has_bp = (child / "testpilot.json").exists()
121+
tp_dir = child / "testpilot"
122+
if not has_bp and tp_dir.is_dir():
123+
has_bp = any(tp_dir.glob("*.json"))
124+
if has_bp:
119125
_preview_dirs[child.name] = child
120126

121127
if _preview_dirs:

src/testing/blueprint.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ class Blueprint(BaseModel):
6060
pages: list[BlueprintPage] = Field(default_factory=list, description="页面列表")
6161
global_elements: dict[str, str] = Field(default_factory=dict, description="全局元素映射(所有页面共享)")
6262
permissions: list[str] = Field(default_factory=list, description="Android权限列表,launch时通过adb批量授权,如 android.permission.CAMERA")
63+
start_command: str = Field(default="", description="应用启动命令(如 npm start / python app.py),纯HTML应用留空使用内置预览服务器")
64+
start_cwd: str = Field(default="", description="启动命令的工作目录(相对于蓝本文件所在目录,默认为蓝本所在项目根目录)")
6365
app_package: str = Field(default="", description="Android应用包名(手机测试时用)")
6466
app_activity: str = Field(default="", description="Android启动Activity(手机测试时用)")
6567

src/testing/blueprint_runner.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ async def run(
9696
blueprint.total_steps,
9797
"开启" if smart_repair_enabled else "关闭")
9898

99+
# v10.2:如果蓝本有 start_command,先启动被测应用并等待就绪
100+
self._app_started_by_runner = False
101+
if getattr(blueprint, "start_command", "") and blueprint.start_command.strip():
102+
await self._auto_start_app(blueprint)
103+
99104
# v2.0:通知控制器测试开始
100105
if self._controller:
101106
self._controller.start(total_steps=blueprint.total_steps)
@@ -638,3 +643,54 @@ def _generate_markdown(
638643
])
639644

640645
return "\n".join(lines)
646+
647+
# ── v10.2:自动启动被测应用 ────────────────────────
648+
649+
async def _auto_start_app(self, blueprint: Blueprint) -> None:
650+
"""根据蓝本中的 start_command 自动启动被测应用并等待就绪。"""
651+
import urllib.request
652+
import urllib.error
653+
from pathlib import Path
654+
655+
cmd = blueprint.start_command.strip()
656+
cwd = blueprint.start_cwd.strip() or "."
657+
base_url = blueprint.base_url.strip()
658+
659+
logger.info("自动启动被测应用 | cmd={} | cwd={} | base_url={}", cmd, cwd, base_url)
660+
661+
# 先检查 base_url 是否已经可访问(应用可能已在运行)
662+
if base_url and await self._check_url_ready(base_url):
663+
logger.info("被测应用已在运行: {}", base_url)
664+
return
665+
666+
# 通过 process_runner 启动应用
667+
from src.testing.process_runner import process_runner
668+
success = await process_runner.start(cmd, cwd)
669+
if not success:
670+
logger.warning("应用启动失败或已在运行: {}", cmd)
671+
return
672+
673+
self._app_started_by_runner = True
674+
675+
# 等待 base_url 可访问(最多30秒)
676+
if base_url:
677+
logger.info("等待应用就绪: {}", base_url)
678+
import asyncio
679+
for i in range(30):
680+
if await self._check_url_ready(base_url):
681+
logger.info("应用已就绪(等待{}秒): {}", i + 1, base_url)
682+
return
683+
await asyncio.sleep(1)
684+
logger.warning("应用启动超时(30秒),继续测试: {}", base_url)
685+
686+
@staticmethod
687+
async def _check_url_ready(url: str) -> bool:
688+
"""检查URL是否可访问。"""
689+
import urllib.request
690+
import urllib.error
691+
try:
692+
req = urllib.request.Request(url, method="HEAD")
693+
with urllib.request.urlopen(req, timeout=2):
694+
return True
695+
except Exception:
696+
return False

0 commit comments

Comments
 (0)