一句话总结:组件库通常隐藏原始 input、用自定义元素替代,然后通过监听自定义元素的事件来同步状态。自动化操作时不能点 input,要点对最外层的容器元素(label/div),有时还需要模拟完整的事件链。
目录
- 陷阱一:单选按钮(Radio Button)
- 陷阱二:自定义选择器(Select/Dropdown)
- 陷阱三:复选框与开关(Checkbox/Switch)
- 通用原则
- 排查方法
- 总结
问题描述
需要选择”发布设置”中的”否”选项。页面中有两个 radio button:”是”和”否”。
直接点击 input 无效:
1 2 3
| js(ws, "document.querySelector('input[value=\"no\"]').click()")
|
原因分析
某组件库的 radio 结构如下:
1 2 3 4
| <label class="arco-radio"> <input type="radio" value="no" hidden> <span class="arco-radio-text">否</span> </label>
|
关键点:
input[type=radio] 设置了 hidden 属性,不可见
- 组件库没有直接使用原始 input 的 change 事件
- 点击
<label> 时,组件库的内部逻辑处理状态变更
对于这种结构,点击被隐藏的 input 本身不会触发组件库的状态管理,因为组件库拦截并重写了交互逻辑。
正确做法
点击最外层的 <label class="arco-radio">:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| def click_radio_label(ws, text): """点击包含指定文本的 radio label""" return js(ws, f""" (() => {{ const labels = document.querySelectorAll('label.arco-radio'); for (const lb of labels) {{ const txt = lb.querySelector('.arco-radio-text'); if (txt && txt.innerText.trim() === '{text}') {{ lb.click(); return true; }} }} return false; }})() """)
click_radio_label(ws, '否') click_radio_label(ws, '是')
|
Ant Design 的 Radio
Ant Design 的 radio 结构类似但选择器不同:
1 2 3 4 5 6 7 8
| <label class="ant-radio-wrapper"> <span class="ant-radio"> <input type="radio" class="ant-radio-input" value="no"> <span class="ant-radio-inner"></span> </span> <span>否</span> </label>
|
对应的做法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| def click_ant_radio(ws, text): return js(ws, f""" (() => {{ const wrappers = document.querySelectorAll('.ant-radio-wrapper'); for (const w of wrappers) {{ if (w.innerText.trim() === '{text}') {{ w.click(); return true; }} }} return false; }})() """)
|
陷阱二:自定义选择器(Select/Dropdown)
组件库的自定义 Select 组件通常不是标准的 <select> 元素,而是一个由按钮 + 弹出面板组成的自定义控件。
问题
直接设置 select 的 value 无效:
1 2
| js(ws, "document.querySelector('select').value = 'option1'")
|
正确做法
自定义 Select 需要点击 → 等待面板弹出 → 选择选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| def select_custom_option(ws, trigger_selector, option_text): """操作自定义 Select 组件""" js(ws, f"document.querySelector('{trigger_selector}').click()") time.sleep(0.5) result = js(ws, f""" (() => {{ const panels = document.querySelectorAll( '.arco-select-popup, .ant-select-dropdown, ' + '[class*="dropdown"], [class*="popup"]' ); for (const panel of panels) {{ if (panel.offsetParent === null) continue; const options = panel.querySelectorAll( '.arco-select-option, .ant-select-item-option, ' + '[class*="option"], [role="option"]' ); for (const opt of options) {{ if (opt.innerText.trim() === '{option_text}') {{ opt.click(); return 'OK'; }} }} }} return 'NOT_FOUND'; }})() """) return result
|
陷阱三:复选框与开关(Checkbox/Switch)
组件库的 Switch 开关组件通常是一个 div + 滑动动画,不是标准的 checkbox:
1 2 3 4
| <div class="arco-switch"> <div class="arco-switch-dot"></div> </div>
|
正确做法
直接点击整个 switch 容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def toggle_switch(ws, switch_selector, want_on=True): """确保开关处于目标状态""" current = js(ws, f""" (() => {{ const sw = document.querySelector('{switch_selector}'); if (!sw) return null; return sw.classList.contains('arco-switch-checked'); }})() """) if current is None: return False if current != want_on: js(ws, f"document.querySelector('{switch_selector}').click()") time.sleep(0.3) return True
|
通用原则
经过多次踩坑,总结出组件库 UI 自动化的通用原则:
1. 点外层容器,不点内部元素
1 2
| ✅ 点 <label class="arco-radio"> → 组件内部处理状态 ❌ 点 <input type="radio" hidden> → 无效
|
2. 按文本找,不按结构找
按结构定位(nth-child、class 组合)在组件库中很不稳定——版本升级或主题变化都可能改变 class 名。
按文本定位更可靠:
1 2 3 4 5 6
| js(ws, """ Array.from(document.querySelectorAll('label, button, div, span')) .find(el => el.innerText.trim() === '目标文本') ?.click() """)
|
3. 使用 offsetParent !== null 筛选可见元素
组件库的弹窗和面板经常在 DOM 中预先渲染但不可见:
1 2 3 4 5 6 7
| js(ws, """ Array.from(document.querySelectorAll('.arco-radio')) .filter(el => el.offsetParent !== null) .find(el => el.innerText.includes('否')) ?.click() """)
|
4. 组件库的交互需要两步
1 2
| 第一步:用 JS click 点击容器 → 这个通常是够的 第二步:如果 JS click 无效 → 换 CDP dispatchMouseEvent
|
排查方法
当点击无效时,按以下顺序排查:
检查元素结构:通过 CDP 获取元素的 outerHTML
1
| html = js(ws, "document.querySelector('.target').outerHTML")
|
检查组件的 JS 事件绑定:看组件监听的是什么事件
1 2 3
| const el = document.querySelector('.target'); getEventListeners(el);
|
检查 visible 状态:
1 2 3
| visible = js(ws, """ document.querySelector('.target').offsetParent !== null """)
|
尝试不同的点击方式(按优先级):
- 点击最外层容器 → 不奏效则…
- 用 CDP dispatchMouseEvent 真实点击 → 不奏效则…
- 模拟完整事件链(mousedown → mouseup → click)
总结
| 组件类型 |
错误做法 |
正确做法 |
| Radio button |
点隐藏的 input |
点 label 容器 |
| 自定义 Select |
设 value 属性 |
点击触发器 → 选选项 |
| Switch 开关 |
设 checked 属性 |
点容器元素 |
| Checkbox |
直接设 .checked |
点 label 或触发 change 事件 |
| 自定义 Dropdown |
直接设值 |
CDP 鼠标点击打开 → 选选项 → Escape 关闭 |
核心原则:组件库封装的 UI 控件,永远优先点击最外层的可见容器元素。如果需要选择具体选项,按文本内容查找而非按 DOM 结构。当 JS click 失效时,升级到 CDP 的 dispatchMouseEvent 真实事件。