# 问题处理文档 - 协议弹窗确定按钮点击无效

> **文档类型**: 问题处理文档
> **创建日期**: 2026-07-17
> **作者**: czj
> **优先级**: P0
> **状态**: 待修复

---

## 一、问题描述

### 1.1 现象

批量执行测试用例时，登录流程无法完成。具体表现为：

1. 执行器访问登录页，填写账号密码验证码，点击登录按钮
2. 协议弹窗正常弹出，"确定"按钮可见
3. Playwright 尝试点击"确定"按钮，但页面无响应
4. 登录流程卡住，后续所有用例因无法登录而失败

### 1.2 复现步骤

1. 启动后端服务 `uvicorn app.main:app --port 8001`
2. 启动前端服务 `npm run dev`
3. 访问 http://localhost:3000/cases/ui
4. 执行任意需要登录的用例（如"会议管理"模块用例）
5. 观察执行日志，协议弹窗步骤失败

### 1.3 已验证的排查

| 尝试方式 | 结果 |
|----------|------|
| Playwright `click()` | 无效 |
| `click(force=True)` 强制点击 | 无效 |
| JS `element.click()` 通过 evaluate | 无效 |
| 键盘 `Enter` | 无效 |
| 手动在浏览器中登录 | 正常 |
| 被测系统服务状态 | 正常 |

### 1.4 影响范围

- 所有需要登录的测试用例均无法执行
- 登录用例本身无法通过
- 自动登录 (`auto_login=True`) 失败
- 手动登录步骤模式也失败

---

## 二、根因分析

### 2.1 协议弹窗选择器问题

#### 2.1.1 登录页存在多个"确定"按钮

根据 `backend/element_selectors.json` 实测数据，登录页有 **2 个"确定"按钮**：

```json
{"text": "确定", "class": ["el-button", "el-button--primary"], "type": "button"},
{"text": "确定", "class": ["el-button", "el-button--primary"], "type": "button"},
{"text": "阅读并同意", "class": ["el-button", "el-button--primary"], "type": "button"}
```

用例步骤中使用的选择器 `button:has-text("确定")` 会匹配到第一个"确定"按钮，但这不一定是协议弹窗中的那个。

#### 2.1.2 选择器策略不一致

| 位置 | 选择器 | 问题 |
|------|--------|------|
| `do_login()` 第 262 行 | `.el-dialog button, [role="dialog"] button` | 先找容器再找按钮，但可能匹配到弹窗外层容器中的其他按钮 |
| 用例步骤 | `button:has-text("确定")` | 无容器限定，可能匹配到页面上非弹窗的"确定"按钮 |
| `selector_mapper.py` confirm_agreement | `.el-dialog button:has-text('确定')` | 精确，但用例数据中未使用 `page_key/element_key` 引用 |

### 2.2 协议弹窗无显式等待

`do_login()` 中的代码（第 259-268 行）：

```python
self._page.wait_for_timeout(3000)  # 仅等待 3 秒

# 处理协议弹窗
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
```

**问题**：
- 没有用 `wait_for_selector` 等待弹窗出现
- 如果弹窗加载慢于 3 秒，会找不到按钮
- 如果弹窗动画未完成，`is_visible()` 可能返回 True 但点击无效

### 2.3 点击可能被事件拦截

被测系统可能使用了以下技术导致 Playwright 点击被拦截：

1. **事件委托** - Vue/Element UI 可能通过父元素监听事件
2. **pointer-events** - CSS 可能临时禁用了指针事件
3. **点击验证** - 前端可能检测点击来源，非真实用户点击则忽略
4. **事件冒泡阻止** - 弹窗可能阻止了事件冒泡

### 2.4 弹窗可能需要特定交互顺序

协议弹窗可能需要：
- 先勾选"我已阅读协议"复选框
- 再点击"确定"按钮

根据 `element_selectors.json`，登录页有一个 checkbox：
```json
{"type": "checkbox", "placeholder": "", "class": ["el-checkbox__original"]}
```

如果 checkbox 未勾选，"确定"按钮点击可能被前端拦截。

---

## 三、修复方案

### 3.1 方案：等待弹窗 + 多选择器回退 + 多种点击方式（推荐）

#### 改动点

| # | 改动 | 位置 | 说明 |
|---|------|------|------|
| 1 | 等待协议弹窗出现 | `do_login()` | 用 `wait_for_selector` 等待 `.el-dialog` 可见 |
| 2 | 使用精确选择器 | `do_login()` | 改用 `.el-dialog .el-dialog__body button:has-text('确定')` |
| 3 | 多种点击方式回退 | `do_login()` | 先 `click()`，失败则 `dispatch_event('click')`，再失败则 JS `click()` |
| 4 | 增加复选框勾选 | `do_login()` | 点击"确定"前先勾选协议复选框（如有） |
| 5 | 增加等待弹窗消失 | `do_login()` | 点击后等待弹窗消失，确认关闭成功 |

#### 详细改动

**修改 `do_login()` 中协议弹窗处理部分**：

修改前（第 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 = self._page.query_selector('.el-dialog .el-checkbox__original, .el-dialog input[type="checkbox"]')
if checkbox and checkbox.is_visible():
    try:
        if not checkbox.is_checked():
            checkbox.click()
            logger.debug("勾选协议复选框")
    except Exception as e:
        logger.debug(f"复选框操作失败（可能不需要）: {e}")

# 多选择器策略 + 多种点击方式
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 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}")
                        continue
    except Exception as e:
        logger.debug(f"选择器 {sel} 失败: {e}")
        continue

if not clicked:
    logger.warning("所有协议弹窗点击方式均失败")

# 等待弹窗消失
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)
```

### 3.2 不采用的方案

| 方案 | 原因 |
|------|------|
| 增加 `click(force=True)` | 已验证无效，force 只跳过可操作性检查，不绕过事件拦截 |
| 增加等待时间到 10 秒 | 治标不治本，弹窗 3 秒内必现，问题在点击方式 |
| 放弃协议弹窗处理 | 登录流程必须完成，否则所有用例无法执行 |

---

## 四、验收标准

| 测试项 | 预期结果 |
|--------|----------|
| 协议弹窗等待 | `wait_for_selector('.el-dialog:visible')` 成功 |
| 协议弹窗点击 | 日志显示"点击协议确认"，无"失败"日志 |
| 协议弹窗关闭 | 日志显示"协议弹窗已关闭" |
| 登录成功判断 | `_is_logged_in = True`，日志显示"自动登录成功" |
| 会议管理 6 用例执行 | 6/6 全部 passed |
| 其他模块用例执行 | 登录步骤正常，用例可执行 |

---

## 五、相关文件

- `backend/app/executors/playwright_executor.py` — 执行器核心（do_login 方法）
- `backend/app/utils/selector_mapper.py` — 选择器映射
- `backend/element_selectors.json` — 登录页元素实测数据
- `backend/scripts/create_login_cases_full.py` — 登录用例生成脚本

---

*本文档由 Claude Code 生成，遵循项目问题处理文档规范。*