# 执行计划：P1 遗留待办 - 验证步骤前端适配与定位器优化

> **文档版本**: v1.0
> **创建日期**: 2026-08-21
> **关联PRD**: `_PRD_需求文档_P1遗留_验证步骤前端适配与定位器优化.md`
> **预计工期**: 分 3 个阶段，每阶段独立交付验证

---

## 一、执行概述

### 1.1 执行目标

处理 HANDOFF 遗留的两个 P1 待办：

1. **前端适配验证步骤的 `verify_method`**：统一验证字段模型（`verify_method` → `verify` 顶层字段），前端补全 `body_not_contains` 断言类型、verify 独立类型选项、IntentStepEditor 新增 verify 编辑区
2. **定位器优化**：`_do_fill` 滚动兜底、二级菜单页面注册（`page_url_mapping.json` 补条目）

**硬性约束**：全部改动向后兼容——存量用例不填新字段、不依赖新机制，必须持续通过（后端 pytest 全量 + 前端 `npm run build` 作为每阶段回归基线）。

### 1.2 改动范围

| 模块 | 文件 | 改动类型 |
|------|------|----------|
| 智能定位 | `backend/app/services/smart_locate_service.py` | 增强（verify_method → verify 顶层字段，3 处） |
| 执行器 | `backend/app/executors/playwright_executor.py` | 增强（`_do_assert` 新增 `body_not_contains`；`_do_fill` 滚动兜底） |
| 前端编辑器 | `frontend/src/components/CaseStepEditor.vue` | 增强（`ASSERT_TYPE_OPTIONS` 加 `body_not_contains`；独立 `VERIFY_TYPE_OPTIONS`） |
| 前端编辑器 | `frontend/src/components/IntentStepEditor.vue` | 增强（新增 verify 折叠编辑区） |
| 前端解析 | `frontend/src/utils/stepParser.ts` | 增强（`toIntentStep`/`toStepDefinition` verify_method → verify 转换） |
| 页面注册 | `backend/app/data/page_url_mapping.json` | 增强（二级菜单条目） |

### 1.3 不改动的部分

- 数据库 schema（`verify` 是 `steps` JSON 列内步骤 dict 字段，无新表/新列）
- 现有动作分发逻辑（13 种 action 不变）
- 现有 `_do_click`/`_do_fill`/`_do_assert` 的成熟回退策略（仅新增分支）
- 现有 `execute_case` 主流程骨架
- 被测系统（5.44/5.69）无改动

---

## 二、任务分解与实施计划

### 阶段 1：后端字段统一 + 断言补全

#### 任务 1.1：`smart_locate_service.py` — verify_method → verify 顶层字段

在 `_locate_single_step` 三处 `result['params']['verify_method']` 赋值后，同步设置 `result['verify']` 顶层字段：

| 位置 | verify_method | 同步设置 verify |
|------|--------------|----------------|
| L597（提示语验证） | `message` | `{"type": "dialog"}` |
| L648（列表搜索验证） | `search` | `{"type": "element_visible"}` |
| L694（目标文本验证） | `text_visible` | `{"type": "text_visible", "text": target_text}` |

**注意**：`result['verify']` 需要在 `result` 字典顶层（与 `success`/`action`/`params` 平级），与 schema 的 `StepDefinition.verify` 顶层字段对应。文本验证处 `target_text` 变量已在作用域内（从步骤名提取），直接引用。

#### 任务 1.2：`playwright_executor.py` — `_do_assert` 新增 `body_not_contains`

在 `_do_assert` 的 `elif assert_type == "element_count_min":` 分支后、`else: raise` 前新增：

```python
elif assert_type == "body_not_contains":
    # 页面/元素文本不应包含指定字符串（防假通过：expected 为空视为失败）
    expected_str = str(params.get("expected") or expected or "").strip()
    if not expected_str:
        raise AssertionError("✗ 断言失败: body_not_contains 期望值不能为空")
    got_text = None
    if selectors:
        # 有选择器 → 取元素文本
        for selector in selectors:
            try:
                el = self._page.wait_for_selector(selector, timeout=self.timeout)
                if el:
                    got_text = el.inner_text()
                    break
            except Exception:
                continue
        if got_text is None:
            # 选择器全失败 → 尝试 iframe
            for selector in selectors:
                for frame in self._page.frames:
                    if frame == self._page.main_frame:
                        continue
                    try:
                        el = frame.wait_for_selector(selector, timeout=5000)
                        if el:
                            got_text = el.inner_text()
                            break
                    except Exception:
                        continue
                if got_text is not None:
                    break
        if got_text is None:
            raise AssertionError(f"✗ 断言失败: body_not_contains 未找到元素: {selectors}")
    else:
        # 无选择器 → 取整页文本
        try:
            got_text = self._page.inner_text("body")
        except Exception:
            got_text = self._page.content()  # 兜底：取 HTML（含脚本标签）
    if expected_str in (got_text or ""):
        raise AssertionError(f"✗ 断言失败: 页面仍包含 [{expected_str}]")
```

#### 任务 1.3：`playwright_executor.py` — `_do_fill` 滚动兜底

在 `_do_fill` 的 CSS/XPath 分支（主页面尝试，L2395-2407）中，`wait_for_selector` 之后、`click` 之前加滚动：

```python
# 滚动到可见区域（确保元素不被遮挡，与 _do_click 一致）
try:
    self._page.locator(selector).first.scroll_into_view_if_needed(timeout=5000)
except Exception:
    pass  # 滚动失败不阻塞后续操作
```

同样在 iframe 分支（L2417）和语义定位器分支（L2359）处视需要补充（主路径 L2395 为最小改动，iframe/micro-app 分支保持原样）。

#### 阶段 1 验证

- [ ] 后端回归：`cd backend && pytest tests/ -v --deselect=预存坏用例`
- [ ] 代码 review：verify 顶层字段三处设置正确、body_not_contains 分支逻辑与 api 版对齐

### 阶段 2：前端验证编辑补全

#### 任务 2.1：`CaseStepEditor.vue` — 两处改动

1. `ASSERT_TYPE_OPTIONS` 新增：`{ label: '页面不包含 (body_not_contains)', value: 'body_not_contains' }`
2. 新增独立 `VERIFY_TYPE_OPTIONS`（与后端 `_build_verify_assert` L2804-2822 对齐）：

```ts
/** verify 类型选项（与后端 _build_verify_assert 对齐，非 assert 类型） */
const VERIFY_TYPE_OPTIONS = [
  { label: '文本可见 (text_visible)', value: 'text_visible' },
  { label: '元素可见 (element_visible)', value: 'element_visible' },
  { label: '文本隐藏 (text_hidden)', value: 'text_hidden' },
  { label: '元素存在 (element_exists)', value: 'element_exists' },
  { label: 'URL 包含 (url_contains)', value: 'url_contains' },
  { label: '弹窗提示 (dialog)', value: 'dialog' },
  { label: '输入值相等 (value)', value: 'value' },
  { label: '元素数量 (count)', value: 'count' },
]
```

3. verify 编辑区（L345-378）的 `<el-select>` 改用 `VERIFY_TYPE_OPTIONS`
4. `verifyNeedsSelector`/`verifyNeedsText` 适配 verify 类型：

```ts
/** 验证是否需要选择器（verify 类型口径） */
const verifyNeedsSelector = (type: string): boolean =>
  ['text_visible', 'element_visible', 'text_hidden', 'element_exists', 'value', 'count'].includes(type || '')

/** 验证是否需要期望文本（verify 类型口径） */
const verifyNeedsText = (type: string): boolean =>
  ['text_visible', 'text_hidden', 'url_contains', 'dialog', 'value', 'count'].includes(type || '')
```

#### 任务 2.2：`IntentStepEditor.vue` — 新增 verify 折叠编辑区

1. template：在「预期结果 / 实际结果」区域后新增折叠高级区（样式复用 CaseStepEditor `.step-advanced` 风格）
2. 字段：verify 类型下拉（VERIFY_TYPE_OPTIONS）+ selector + text + timeout + must
3. script：新增 `getVerify(step)` / `verifyNeedsSelector` / `verifyNeedsText` / `advancedOpen` 状态，`IntentStep` 接口（`stepParser.ts` 已声明 `verify?: Record<string, any>`）直接绑定
4. 新增折叠文案：`hasVerify(step)` 存在时显示「状态验证」否则「记录业务意图（可选）」

#### 任务 2.3：`stepParser.ts` — verify_method → verify 转换

1. `toIntentStep`（编辑回显）：`params.verify_method` 有值时转换为 `intentStep.verify`：

```ts
// P1：verify_method 兼容转换（智能定位旧产出）
if (!intentStep.verify && step.params?.verify_method) {
  const vm = step.params.verify_method
  const text = step.params?.expected_text || step.params?.expected || ''
  if (vm === 'message') intentStep.verify = { type: 'dialog' }
  else if (vm === 'search') intentStep.verify = { type: 'element_visible', selector: step.params?.selector || '' }
  else if (vm === 'text_visible') intentStep.verify = { type: 'text_visible', text }
}
```

2. `toStepDefinition`（保存）：`params.verify_method` 有值时同步输出 `verify` 顶层字段（同上映射），避免仅编辑不保存丢失

#### 阶段 2 验证

- [ ] 前端构建：`cd frontend && npm run build`
- [ ] 页面检查：CaseStepEditor assert 下拉含 body_not_contains；verify 区独立类型；IntentStepEditor 折叠高级区可编辑
- [ ] round-trip：含 verify 用例保存后重载不丢

### 阶段 3：定位器优化（二级菜单注册）

#### 任务 3.1：`page_url_mapping.json` — 补充二级菜单条目

新增 19 个二级菜单条目（结构先行，URL 部分实测后精化）：

| 二级菜单 | 所属分类 | 推荐 url_patterns（结构占位） |
|---------|---------|------|
| 壁纸推送/脚本命令/应用管理/屏幕管理/软件卸载/设备网络/门口屏发布/播放器发布 | 集控控制 | 等 `/sut-explore` 实测 |
| 资产信息/资产设备 | 资产管理 | 同上 |
| 运维设备/运维巡检 | 会议运维 | 同上 |
| 工单列表/我的工单/告警工单 | 维护工单 | 同上 |
| 信息窗管理/消息通知 | 信息管理 | 同上 |
| 会务统筹/会务工单 | 会务管理 | 同上 |

**实现步骤**：
1. 若已从 `DRAWER_MENU_HIERARCHY`/`MENU_BLOCK_ROUTES` 获得已知 URL → 填入 `url` + `url_patterns`（`meetingV3.*{Page}` 模式）
2. URL 未知的 → 仅添加 `name` + `match_rules`（`step_name_contains` + `action_is: [click]`），`url_patterns`/`fingerprint` 留空，走 `skip_steps` 跳过点击菜单？——**不**：未知页面不应加 skip_steps（会导致步骤被错误跳过），仅加 match_rules 用于 scope 识别
3. 新增条目后需重启 uvicorn（`page_url_mapping.json` 单例不热加载）

#### 任务 3.2（可选，若实测）：验证二级菜单直达

用 `/sut-explore` skill 访问被测系统，实测 2-3 个二级菜单的真实 URL 并回填，验证 navigate 直达 + scope 识别。

#### 阶段 3 验证

- [ ] `page_url_mapping.json` 语法正确（`python -c "import json; json.load(open(...))"`）
- [ ] 后端回归 pytest 通过（页面识别相关已有 `test_page_url_service.py`）
- [ ] 实测 1 个二级菜单用例执行（若完成任务 3.2）

---

## 三、风险与注意事项

| 风险 | 缓解 |
|------|------|
| 二级菜单 URL 未知，注册表可能误配 | 本阶段以 match_rules 为主（scope 识别），url_patterns/fingerprint 实测后补，宁可少配不可误配 |
| verify 拆分后存量 verify 数据（type=visible 等 assert 类型）兼容 | 前端仅换下拉选项源，`step.verify.type` 值不变；`_build_verify_assert` 对未知类型 fallback 到 `visible`（L2822），存量安全 |
| `body_not_contains` 无 selector 时取整页文本可能误判 | 推荐带 selector 使用；无 selector 时与 api 版（response_body）语义类比，可接受 |
| `page_url_mapping.json` 单例不热加载 | 修改后必须重启 uvicorn 进程 |

---

## 四、交付物清单

| # | 文件 | 状态 |
|---|------|------|
| 1 | `Docs/PRD/需求文档/用例管理/_PRD_需求文档_P1遗留_验证步骤前端适配与定位器优化.md` | ✅ 已交付 |
| 2 | 本文档（执行计划） | ✅ 已交付 |
| 3 | `backend/app/services/smart_locate_service.py` | 待实现 |
| 4 | `backend/app/executors/playwright_executor.py` | 待实现 |
| 5 | `frontend/src/components/CaseStepEditor.vue` | 待实现 |
| 6 | `frontend/src/components/IntentStepEditor.vue` | 待实现 |
| 7 | `frontend/src/utils/stepParser.ts` | 待实现 |
| 8 | `backend/app/data/page_url_mapping.json` | 待实现 |