一句话总结:组件库通常隐藏原始 input、用自定义元素替代,然后通过监听自定义元素的事件来同步状态。自动化操作时不能点 input,要点对最外层的容器元素(label/div),有时还需要模拟完整的事件链。


目录

  1. 陷阱一:单选按钮(Radio Button)
  2. 陷阱二:自定义选择器(Select/Dropdown)
  3. 陷阱三:复选框与开关(Checkbox/Switch)
  4. 通用原则
  5. 排查方法
  6. 总结

陷阱一:单选按钮(Radio Button)

问题描述

需要选择”发布设置”中的”否”选项。页面中有两个 radio button:”是”和”否”。

直接点击 input 无效:

1
2
3
# ❌ 错误做法:点击隐藏的 input
js(ws, "document.querySelector('input[value=\"no\"]').click()")
# 点了,但没变化

原因分析

某组件库的 radio 结构如下:

1
2
3
4
<label class="arco-radio">
<input type="radio" value="no" hidden> <!-- 实际 input 被隐藏 -->
<span class="arco-radio-text"></span> <!-- 可见文本 -->
</label>

关键点:

  1. input[type=radio] 设置了 hidden 属性,不可见
  2. 组件库没有直接使用原始 input 的 change 事件
  3. 点击 <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
<!-- Ant Design 的 radio 结构 -->
<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
# ❌ 标准 select 的做法,对自定义 Select 无效
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 组件"""
# 1. 点击触发器打开下拉面板
js(ws, f"document.querySelector('{trigger_selector}').click()")
time.sleep(0.5)

# 2. 在下拉面板中选择选项
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
<!-- Switch 开关结构 -->
<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-childclass 组合)在组件库中很不稳定——版本升级或主题变化都可能改变 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

排查方法

当点击无效时,按以下顺序排查:

  1. 检查元素结构:通过 CDP 获取元素的 outerHTML

    1
    html = js(ws, "document.querySelector('.target').outerHTML")
  2. 检查组件的 JS 事件绑定:看组件监听的是什么事件

    1
    2
    3
    // 在页面控制台查看
    const el = document.querySelector('.target');
    getEventListeners(el); // 需要 DevTools 中运行
  3. 检查 visible 状态

    1
    2
    3
    visible = js(ws, """
    document.querySelector('.target').offsetParent !== null
    """)
  4. 尝试不同的点击方式(按优先级):

    • 点击最外层容器 → 不奏效则…
    • 用 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 真实事件。