# 执行计划 - 修复协议弹窗确定按钮点击无效

> **文档类型**: 执行计划文档
> **创建日期**: 2026-07-17
> **作者**: czj
> **关联文档**: `_问题处理_协议弹窗确定按钮点击无效.md`
> **状态**: 待执行

---

## 一、改动总览

| # | 改动项 | 文件/范围 | 改动量 | 说明 |
|---|--------|-----------|--------|------|
| 1 | 等待协议弹窗出现 | `playwright_executor.py` do_login() | ~5 行 | 用 wait_for_selector 等待 .el-dialog 可见 |
| 2 | 增加复选框勾选 | `playwright_executor.py` do_login() | ~8 行 | 点击确定前先勾选协议复选框 |
| 3 | 多选择器 + 多点击方式 | `playwright_executor.py` do_login() | ~50 行 | 6 个选择器回退 + 3 种点击方式回退 |
| 4 | 等待弹窗消失 | `playwright_executor.py` do_login() | ~5 行 | 点击后等待 .el-dialog hidden |
| 5 | 调整等待时间 | `playwright_executor.py` do_login() | ~1 行 | 弹窗后等待从 5000ms 调整为 2000ms |

**总计**：1 个代码文件改动，约 70 行新增/修改。

---

## 二、详细执行步骤

### Step 1：修改 do_login() 协议弹窗处理逻辑

**文件**：`backend/app/executors/playwright_executor.py`

#### 1.1 替换协议弹窗处理代码

**修改前**（第 261-270 行）：

```python
# 处理协议弹窗
dialogs = self._page.query_selector_all('.el-dialog button, [role="dialog"] button')
for db in dialogs:
    text = (db.inner_text() or '').strip()
    if ('确定' in text or '同意' in text) and db.is_visible():
        db.click()
        logger.debug(f"点击协议确认: {text}")
        break

self._page.wait_for_timeout(5000)
```

**修改后**：

```python
# === 等待协议弹窗出现（最多等待 10 秒） ===
try:
    self._page.wait_for_selector('.el-dialog:visible', timeout=10000)
    logger.debug("协议弹窗已出现")
except Exception as e:
    logger.warning(f"等待协议弹窗超时（可能无弹窗）: {e}")

# === 勾选协议复选框（如有） ===
checkbox_selectors = [
    '.el-dialog .el-checkbox__original',
    '.el-dialog input[type="checkbox"]',
    '.el-checkbox__original',
]
for cb_sel in checkbox_selectors:
    try:
        cb = self._page.query_selector(cb_sel)
        if cb and cb.is_visible():
            try:
                if not cb.is_checked():
                    cb.click()
                    logger.debug(f"勾选协议复选框: {cb_sel}")
                    self._page.wait_for_timeout(500)
            except Exception as e:
                logger.debug(f"复选框操作失败（可能不需要）: {e}")
            break
    except Exception:
        continue

# === 多选择器 + 多种点击方式回退 ===
confirm_selectors = [
    ".el-dialog .el-dialog__body button:has-text('确定')",
    ".el-dialog button.el-button--primary:has-text('确定')",
    ".el-dialog button:has-text('确定')",
    ".el-dialog button:has-text('同意')",
    ".el-dialog button:has-text('阅读并同意')",
    "[role='dialog'] button:has-text('确定')",
]

clicked = False
for sel in confirm_selectors:
    try:
        btn = self._page.query_selector(sel)
        if btn and btn.is_visible():
            # 方式1：普通点击
            try:
                btn.click(timeout=5000)
                logger.debug(f"协议确认-普通点击成功: {sel}")
                clicked = True
                break
            except Exception as e1:
                logger.debug(f"协议确认-普通点击失败: {e1}")
            # 方式2：dispatch_event
            try:
                btn.dispatch_event('click')
                logger.debug(f"协议确认-dispatch_event成功: {sel}")
                clicked = True
                break
            except Exception as e2:
                logger.debug(f"协议确认-dispatch_event失败: {e2}")
            # 方式3：JS click
            try:
                self._page.evaluate('(el) => el.click()', btn)
                logger.debug(f"协议确认-JS click成功: {sel}")
                clicked = True
                break
            except Exception as e3:
                logger.debug(f"协议确认-JS click失败: {e3}")
    except Exception as e:
        logger.debug(f"选择器 {sel} 查询失败: {e}")

if not clicked:
    logger.warning("所有协议弹窗点击方式均失败，尝试最后手段：键盘 Enter")
    try:
        self._page.keyboard.press('Enter')
        logger.debug("协议确认-键盘 Enter")
    except Exception as e:
        logger.warning(f"键盘 Enter 也失败: {e}")

# === 等待弹窗消失 ===
try:
    self._page.wait_for_selector('.el-dialog', state='hidden', timeout=5000)
    logger.debug("协议弹窗已关闭")
except Exception as e:
    logger.debug(f"等待弹窗关闭超时（可能已关闭）: {e}")

self._page.wait_for_timeout(2000)
```

**改动说明**：

1. **等待弹窗出现**：用 `wait_for_selector('.el-dialog:visible')` 显式等待弹窗可见，替代固定 3 秒等待
2. **勾选复选框**：点击"确定"前先检查并勾选协议复选框，解决可能的前端校验
3. **6 级选择器回退**：从最精确到最宽松，逐步尝试
4. **3 种点击方式回退**：普通 click → dispatch_event → JS evaluate click
5. **键盘 Enter 兜底**：所有点击方式失败后尝试键盘回车
6. **等待弹窗消失**：点击后确认弹窗关闭，而非固定等待 5 秒
7. **缩短后置等待**：从 5000ms 改为 2000ms（弹窗关闭后无需长等）

### Step 2：同步更新用例步骤中的协议弹窗选择器

**文件**：`backend/scripts/create_login_cases_full.py`

#### 2.1 更新 AGREE_STEP 选择器

**修改前**（第 200-204 行）：

```python
AGREE_STEP = {
    "name": "确认同意协议弹窗",
    "action": "click",
    "params": {"selector": 'button:has-text("确定")'},
}
```

**修改后**：

```python
AGREE_STEP = {
    "name": "确认同意协议弹窗",
    "action": "click",
    "params": {
        "selectors": [
            ".el-dialog button.el-button--primary:has-text('确定')",
            ".el-dialog button:has-text('确定')",
            ".el-dialog button:has-text('同意')",
            ".el-dialog button:has-text('阅读并同意')",
            "button:has-text('确定')",
        ]
    },
}
```

**改动说明**：
- 将单选择器 `selector` 改为多选择器列表 `selectors`，利用执行器的多选择器回退机制
- 优先匹配弹窗内的按钮，最后回退到无容器限定的选择器

### Step 3：验证修复效果

1. 在 8001 端口启动后端
2. 执行"会议管理"模块用例
3. 检查执行日志，确认协议弹窗处理正常
4. 验证 6/6 用例全部通过

---

## 三、验证步骤

### 3.1 代码验证

| 步骤 | 操作 | 预期结果 |
|------|------|----------|
| 1 | grep "wait_for_selector.*el-dialog" playwright_executor.py | 有匹配（等待弹窗逻辑） |
| 2 | grep "confirm_selectors" playwright_executor.py | 有匹配（多选择器列表） |
| 3 | grep "dispatch_event" playwright_executor.py | 有匹配（点击方式回退） |
| 4 | grep "el-checkbox" playwright_executor.py | 有匹配（复选框勾选） |

### 3.2 功能验证

| 步骤 | 操作 | 预期结果 |
|------|------|----------|
| 1 | 启动后端 + 前端 | 服务正常，8001/health 返回 healthy |
| 2 | 执行"会议管理"模块 6 用例 | 登录成功，6/6 全部 passed |
| 3 | 查看首用例步骤日志 | 显示"协议弹窗已出现"+"协议确认-xxx点击成功"+"协议弹窗已关闭" |
| 4 | 执行其他模块用例 | 登录步骤正常，用例可执行 |

### 3.3 回归验证

| 步骤 | 操作 | 预期结果 |
|------|------|----------|
| 1 | 执行"数据分析"模块用例 | 登录正常，用例执行无异常 |
| 2 | 执行"运维集控"模块用例 | 登录正常，用例执行无异常 |
| 3 | 批量执行全部 29 个用例 | 登录流程稳定，无卡死 |

---

## 四、风险评估

| 风险 | 概率 | 影响 | 缓解 |
|------|------|------|------|
| 协议弹窗结构变化导致选择器失效 | 低 | 登录失败 | 6 级选择器回退覆盖多种弹窗结构 |
| 复选框不存在导致异常 | 低 | 无影响 | 查询失败时 continue 跳过 |
| wait_for_selector 超时（无弹窗场景） | 低 | 额外等待 10 秒 | 超时后 warning 继续执行，不中断流程 |
| 多种点击方式均失败 | 低 | 登录失败 | 键盘 Enter 兜底 + 日志告警 |
| 修改影响其他弹窗处理逻辑 | 低 | 其他弹窗异常 | 仅修改 do_login() 中的协议弹窗处理，不影响其他动作 |

---

*本文档由 Claude Code 生成，遵循项目执行计划文档规范。*