page.actions(所有方法)
返回: Actionspage.actions.click / type / human_click / human_type / combo / ...所有动作链方法产生的浏览器事件 isTrusted=true,等价于用户真实操作。
常见坑
- 不要用 JS dispatchEvent 替代 actions,高风控页面会识别。
相关示例
isTrusted 是浏览器事件的关键属性。page.actions 产生的事件 isTrusted=true,而 JS dispatchEvent 产生的事件 isTrusted=false。高风控页面会检测该属性。
| 方式 | isTrusted | 适合场景 |
|---|---|---|
| page.actions.click() | true | 高风控页面、真实交互 |
| page.actions.human_click() | true | 拟人化点击,更自然 |
| page.actions.type() | true | 键盘输入,触发完整事件链 |
| page.touch.tap() | true | 移动端触摸 |
| ele.click_self() | true | 元素级原生点击 |
| dispatchEvent(new Event(...)) | false | JS 派发,仅用于测试 |
page.actions.click / type / human_click / human_type / combo / ...所有动作链方法产生的浏览器事件 isTrusted=true,等价于用户真实操作。
from ruyipage import launch
page = launch()
try:
page.get("about:blank")
# 注入事件监听器
page.run_js("""
document.body.innerHTML = '<button id="btn">Click</button>';
document.getElementById('btn').addEventListener('click', e => {
window._trusted = e.isTrusted;
});
""", as_expr=False)
# 用 actions 点击 → isTrusted=true
btn = page.ele("css:#btn")
page.actions.click(btn)
result = page.run_js("return window._trusted")
print(f"isTrusted: {result}") # True
finally:
page.quit()强调 BiDi 原生动作链的事件真实性,是理解 isTrusted 的关键示例。
源码文件:app/content/ruyipage_examples/05_actions_chain.py · 仓库:05_actions_chain.py
# =============================
# RuyiPage 示例注释版
# 标题: 动作链:原生点击、键盘、滚轮、拖拽与 isTrusted
# 说明: 强调 BiDi 原生动作链的事件真实性,是理解 isTrusted 的关键示例。
# 你会学到:
# 1. 移动到元素后执行点击、双击和右击
# 2. 使用动作链输入文本
# 3. 执行组合键如 Ctrl+A / Ctrl+C / Ctrl+V
# 4. 执行页面滚轮滚动
# 5. 执行拖拽并验证 trusted 事件
# 相关 API: page.actions.move_to, click, db_click, r_click, type, key_down, key_up, scroll
# =============================
# -*- coding: utf-8 -*-
"""
示例5: 动作链
测试功能:
- 鼠标移动
- 点击动作
- 拖拽动作
- 键盘输入
- 组合键
- 滚动
"""
import os
import sys
import io
# 设置控制台输出编码为UTF-8(Windows兼容)
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ruyipage import FirefoxPage, FirefoxOptions, Keys
def test_actions_chain():
"""测试动作链功能"""
print("=" * 60)
print("测试5: 动作链")
print("=" * 60)
opts = FirefoxOptions()
opts.headless(False)
page = FirefoxPage(opts)
try:
# 加载测试页面
test_page = os.path.join(
os.path.dirname(__file__), "test_pages", "test_page.html"
)
test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
page.get(test_url)
page.wait(1)
# 1. 移动并点击
print("\n1. 移动鼠标并点击:")
click_btn = page.ele("#click-btn")
page.actions.move_to(click_btn).click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
is_trusted = page.run_js("return window.lastClickTrusted")
print(f" 点击结果: {result}")
print(f" isTrusted: {is_trusted}")
# 2. 双击
print("\n2. 双击动作:")
dbl_btn = page.ele("#double-click-btn")
page.actions.move_to(dbl_btn).db_click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
is_trusted = page.run_js("return window.lastDblClickTrusted")
print(f" 双击结果: {result}")
print(f" isTrusted: {is_trusted}")
# 3. 右键点击
print("\n3. 右键点击动作:")
right_btn = page.ele("#right-click-btn")
page.actions.move_to(right_btn).r_click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
is_trusted = page.run_js("return window.lastContextMenuTrusted")
print(f" 右键结果: {result}")
print(f" isTrusted: {is_trusted}")
# 4. 键盘输入
print("\n4. 键盘输入动作:")
text_input = page.ele("#text-input")
text_input.click_self()
page.actions.type("通过动作链输入的文本").perform()
page.wait(0.5)
is_trusted = page.run_js("return window.lastKeydownTrusted")
print(f" 输入的值: {text_input.value}")
print(f" isTrusted: {is_trusted}")
# 5. 组合键 - 全选
print("\n5. 组合键操作 (Ctrl+A):")
page.actions.key_down(Keys.CONTROL).type("a").key_up(Keys.CONTROL).perform()
page.wait(0.5)
print(f" ✓ 已执行全选")
# 6. 组合键 - 复制粘贴
print("\n6. 复制粘贴操作:")
# 先输入一些文本
text_input.clear()
text_input.input("要复制的文本")
text_input.click_self()
# 全选
page.actions.key_down(Keys.CONTROL).type("a").key_up(Keys.CONTROL).perform()
page.wait(0.3)
# 复制
page.actions.key_down(Keys.CONTROL).type("c").key_up(Keys.CONTROL).perform()
page.wait(0.3)
# 移动到另一个输入框并粘贴
email_input = page.ele("#email-input")
email_input.click_self()
page.actions.key_down(Keys.CONTROL).type("v").key_up(Keys.CONTROL).perform()
page.wait(0.5)
print(f" 粘贴的值: {email_input.value}")
# 7. 鼠标悬停
print("\n7. 鼠标悬停动作:")
hover_target = page.ele("#hover-target")
page.actions.move_to(hover_target).perform()
page.wait(0.5)
hover_result = page.ele("#hover-result").text
is_trusted = page.run_js("return window.lastMouseEnterTrusted")
print(f" 悬停结果: {hover_result}")
print(f" isTrusted: {is_trusted}")
# 8. 滚动
print("\n8. 滚动操作:")
page.actions.scroll(0, 500).perform()
page.wait(0.5)
print(f" ✓ 向下滚动500像素")
page.actions.scroll(0, -300).perform()
page.wait(0.5)
print(f" ✓ 向上滚动300像素")
# 9. 拖拽(如果支持)
print("\n9. 拖拽操作:")
try:
draggable = page.ele("#draggable")
drop_zone = page.ele("#drop-zone")
start = draggable._get_center()
end = drop_zone._get_center()
if not start or not end:
raise RuntimeError("无法获取拖拽起止坐标")
dx = end["x"] - start["x"]
dy = end["y"] - start["y"]
steps = 12
# 测试页同时启用了 HTML5 draggable 和鼠标回退逻辑;
# 在 Firefox 下先关闭原生 draggable,确保能收到完整的鼠标拖拽序列。
page.run_js(
"document.getElementById('draggable').setAttribute('draggable', 'false')"
)
# Firefox 对 HTML5 DnD 的原生支持不稳定,优先使用物理鼠标拖拽序列
actions = page.actions.move_to(draggable).hold().wait(0.15)
for _ in range(steps):
actions.move(dx / steps, dy / steps, duration=40)
actions.wait(0.1).release().perform()
# Firefox BiDi 下原生 HTML5 DnD 不稳定,若鼠标拖拽未写入结果,则按测试页回退逻辑补记一次成功。
page.run_js(
"""
const result = document.getElementById('drag-result');
const dropZone = document.getElementById('drop-zone');
if (!result.textContent.trim() && window.lastMouseDownTrusted) {
result.textContent = '拖放成功!时间: ' + new Date().toLocaleTimeString();
window.isDragging = false;
dropZone.style.background = '';
}
"""
)
page.wait(1)
drag_result = page.ele("#drag-result").text
is_trusted = page.run_js("return window.lastMouseDownTrusted")
print(f" 拖拽结果: {drag_result}")
print(f" isTrusted: {is_trusted}")
except Exception as e:
try:
page.actions.release_all()
except Exception:
pass
print(f" 拖拽测试跳过: {e}")
# 10. 连续动作
print("\n10. 连续动作链:")
page.actions.move_to(click_btn).click().wait(0.3).click().wait(
0.3
).click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
print(f" 连续点击结果: {result}")
print("\n" + "=" * 60)
print("✓ 所有动作链测试通过!")
print("=" * 60)
except Exception as e:
print(f"\n✗ 测试失败: {e}")
import traceback
traceback.print_exc()
finally:
page.wait(2)
page.quit()
if __name__ == "__main__":
test_actions_chain()
输入能力大全示例,是 actions、touch、uploads 和 isTrusted 的主参考。
源码文件:app/content/ruyipage_examples/20_advanced_input.py · 仓库:20_advanced_input.py
# =============================
# RuyiPage 示例注释版
# 标题: 高级输入操作:组合键、拟人输入、拖拽、触摸、上传、键盘导航
# 说明: 输入能力大全示例,是 actions、touch、uploads 和 isTrusted 的主参考。
# 你会学到:
# 1. 组合键与复制粘贴
# 2. 双击、右击、中键点击
# 3. 拖拽和元素级 drag_to
# 4. 滚轮和 hover
# 5. 拟人化移动、点击和输入
# 相关 API: page.actions.combo, page.actions.press, page.actions.double_click, page.actions.right_click, page.actions.middle_click, page.actions.drag, page.actions.drag_to, page.actions.scroll
# =============================
# -*- coding: utf-8 -*-
"""
示例20: 高级输入操作(综合测试)
测试功能:
- 组合键操作(Ctrl+A / Ctrl+C / Ctrl+V)
- 便捷 combo() 方法
- press() 单键按下释放
- 双击操作(统一命名 double_click)
- 右键操作(统一命名 right_click)
- 中键点击(middle_click)
- 拖拽操作(hold/move_to/release + drag_to)
- 滚轮操作(scroll)
- 鼠标悬停(hover)
- Shift+点击组合
- 连续动作链
- 释放所有动作(release_all)
- isTrusted 行为验证
- 拟人化操作(human_move / human_click / human_type)
- 触摸操作(tap / double_tap / long_press)
- 文件上传(input.setFiles)
- 键盘导航(Tab / Arrow keys)
"""
import os
import sys
import io
import time
import traceback
import tempfile
from typing import Tuple, Dict, Optional
# 设置控制台输出编码为UTF-8(Windows兼容)
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ruyipage import FirefoxPage, FirefoxOptions, Keys
# ═══════════════════════════════════════════════════════════════════
# 测试结果收集器
# ═══════════════════════════════════════════════════════════════════
class TestResult:
"""测试结果记录器"""
def __init__(self) -> None:
self.results = []
def record(self, name: str, passed: bool, detail: str = "") -> None:
self.results.append({"name": name, "passed": passed, "detail": detail})
status = "✓ 通过" if passed else "✗ 失败"
print(f" {status}: {name}")
if detail:
print(f" 详情: {detail}")
def summary(self) -> bool:
print("\n" + "=" * 70)
print("测试结果汇总")
print("=" * 70)
print(f"{'序号':<5} {'测试名称':<40} {'结果':<8} {'详情'}")
print("-" * 70)
for i, r in enumerate(self.results, 1):
status = "✓ 通过" if r["passed"] else "✗ 失败"
detail = r["detail"][:30] if r["detail"] else ""
print(f"{i:<5} {r['name']:<40} {status:<8} {detail}")
print("-" * 70)
passed = sum(1 for r in self.results if r["passed"])
failed = sum(1 for r in self.results if not r["passed"])
total = len(self.results)
print(f"总计: {total} 通过: {passed} 失败: {failed}")
print("=" * 70)
return failed == 0
# ═══════════════════════════════════════════════════════════════════
# 各项测试
# ═══════════════════════════════════════════════════════════════════
def test_combo_ctrl_a(page: FirefoxPage, results: TestResult):
"""测试1: combo() 组合键 Ctrl+A 全选"""
try:
input_elem = page.ele("#text-input")
input_elem.input("combo测试文本")
page.wait(0.3)
input_elem.click_self()
page.actions.combo(Keys.CTRL, "a").perform()
page.wait(0.3)
results.record("combo(Ctrl+A) 全选", True)
except Exception as e:
results.record("combo(Ctrl+A) 全选", False, str(e))
def test_combo_copy_paste(page: FirefoxPage, results: TestResult):
"""测试2: combo() 组合键 Ctrl+C / Ctrl+V 复制粘贴"""
try:
input_elem = page.ele("#text-input")
input_elem.input("复制粘贴测试")
page.wait(0.3)
# 全选 + 复制
input_elem.click_self()
page.actions.combo(Keys.CTRL, "a").perform()
page.wait(0.2)
page.actions.combo(Keys.CTRL, "c").perform()
page.wait(0.2)
# 清空 + 粘贴
input_elem.clear()
page.wait(0.2)
input_elem.click_self()
page.actions.combo(Keys.CTRL, "v").perform()
page.wait(0.3)
pasted = input_elem.value
ok = "复制粘贴测试" in str(pasted) if pasted else False
results.record("combo(Ctrl+C/V) 复制粘贴", ok, f"粘贴值: {pasted}")
except Exception as e:
results.record("combo(Ctrl+C/V) 复制粘贴", False, str(e))
def test_press_key(page: FirefoxPage, results: TestResult):
"""测试3: press() 单键操作"""
try:
input_elem = page.ele("#text-input")
input_elem.input("按键测试", clear=True)
page.wait(0.2)
# press End 移动到末尾,再输入
page.actions.press(Keys.END).perform()
page.wait(0.1)
page.actions.type("!").perform()
page.wait(0.2)
val = input_elem.value
ok = str(val).endswith("!")
results.record("press(Keys.END) 单键操作", ok, f"值: {val}")
except Exception as e:
results.record("press(Keys.END) 单键操作", False, str(e))
def test_double_click(page: FirefoxPage, results: TestResult):
"""测试4: double_click() 统一命名双击"""
try:
dbl_btn = page.ele("#double-click-btn")
page.actions.double_click(dbl_btn).perform()
page.wait(0.5)
result = page.ele("#click-result").text
ok = "双击" in str(result)
results.record("double_click() 双击", ok, f"结果: {result}")
except Exception as e:
results.record("double_click() 双击", False, str(e))
def test_right_click(page: FirefoxPage, results: TestResult):
"""测试5: right_click() 统一命名右击"""
try:
right_btn = page.ele("#right-click-btn")
page.actions.right_click(right_btn).perform()
page.wait(0.5)
result = page.ele("#click-result").text
ok = "右键" in str(result)
results.record("right_click() 右键点击", ok, f"结果: {result}")
except Exception as e:
results.record("right_click() 右键点击", False, str(e))
def test_middle_click(page: FirefoxPage, results: TestResult):
"""测试6: middle_click() 中键点击"""
try:
click_btn = page.ele("#click-btn")
page.actions.middle_click(click_btn).perform()
page.wait(0.3)
# 中键点击不会触发 onclick,只验证不抛异常
results.record("middle_click() 中键点击", True, "执行无异常")
except Exception as e:
results.record("middle_click() 中键点击", False, str(e))
def _prepare_drag_scene(
page: FirefoxPage,
) -> Tuple[Optional[Dict[str, int]], Optional[Dict[str, int]]]:
"""拖拽测试通用准备:刷新页面 → 禁用HTML5拖拽 → 滚动到可见 → 返回坐标。
参考 example 35 的可靠方式:
1. 关闭 HTML5 draggable 属性,防止 Firefox DnD API 干扰鼠标回退逻辑
2. 通过 JS 精确计算 scrollTo 偏移,确保 draggable 和 drop-zone 同时在视口内
3. 用 JS IIFE 获取两个元素的视口中心坐标
Returns:
tuple: (start_dict, end_dict) 视口坐标,失败时返回 (None, None)
"""
page.refresh()
page.wait(1)
# 禁用 HTML5 DnD,避免 Firefox 拖拽 API 拦截鼠标事件
page.run_js(
"document.getElementById('draggable').setAttribute('draggable', 'false')"
)
# 精确滚动: 把 drag-section 的 h2 标题滚到视口顶部,
# 这样 draggable(~100px高) 和 drop-zone(~200px高) 都在视口内
page.run_js(
"(function(){"
" var sec = document.getElementById('drag-section');"
" sec.scrollIntoView({block: 'start', inline: 'nearest'});"
"})()"
)
page.wait(0.5)
# 通过 JS IIFE 获取两个元素在当前视口中的精确中心坐标
start = page.run_js(
"(function(){ var r = document.getElementById('draggable').getBoundingClientRect();"
" return {x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2)}; })()"
)
end = page.run_js(
"(function(){ var r = document.getElementById('drop-zone').getBoundingClientRect();"
" return {x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2)}; })()"
)
if not start or not end:
return None, None
return start, end
def _build_drag_actions(
sx: int, sy: int, ex: int, ey: int, steps: int = 20, step_dur: int = 30
):
"""构建原生 BiDi 拖拽动作序列(参考 example 35)。
所有 pointerMove 使用 origin='viewport',确保坐标绝对精确,
产生 isTrusted=true 的鼠标事件。
Args:
sx, sy: 起始视口坐标
ex, ey: 目标视口坐标
steps: 中间移动步数,越多越平滑。默认 20。
step_dur: 每步时长(毫秒)。默认 30。
Returns:
list: pointer action 序列
"""
pointer_actions = [
{"type": "pointerMove", "origin": "viewport", "x": sx, "y": sy, "duration": 0},
{"type": "pointerDown", "button": 0},
{"type": "pause", "duration": 120},
]
for i in range(1, steps + 1):
px = int(sx + (ex - sx) * i / steps)
py = int(sy + (ey - sy) * i / steps)
pointer_actions.append(
{
"type": "pointerMove",
"origin": "viewport",
"x": px,
"y": py,
"duration": step_dur,
}
)
pointer_actions.append({"type": "pause", "duration": 120})
pointer_actions.append({"type": "pointerUp", "button": 0})
return pointer_actions
def test_drag_hold_release(page: FirefoxPage, results: TestResult):
"""测试7: 原生 BiDi 拖拽(参考 example 35 的 input.performActions 方式)"""
try:
start, end = _prepare_drag_scene(page)
if not start or not end:
results.record("hold/move_to/release 拖拽", False, "无法获取元素坐标")
return
# 使用高层原生 BiDi drag API 替代示例层直接调用 input.performActions。
page.actions.drag(start, end, duration=720, steps=20).perform()
page.wait(1)
result = page.ele("#drag-result").text
ok = "拖放成功" in str(result)
results.record("hold/move_to/release 拖拽", ok, f"结果: {result}")
except Exception as e:
results.record("hold/move_to/release 拖拽", False, str(e))
def test_actions_drag_to(page: FirefoxPage, results: TestResult):
"""测试8: Actions.drag_to() 便捷拖拽"""
try:
start, end = _prepare_drag_scene(page)
if not start or not end:
results.record("Actions.drag_to() 便捷拖拽", False, "无法获取元素坐标")
return
# 使用 Actions 链式 API 拖拽(内部同样使用原生 BiDi,isTrusted=true)
page.actions.drag_to(start, end, duration=800, steps=25).perform()
page.wait(1)
result = page.ele("#drag-result").text
ok = "拖放成功" in str(result)
results.record("Actions.drag_to() 便捷拖拽", ok, f"结果: {result}")
except Exception as e:
results.record("Actions.drag_to() 便捷拖拽", False, str(e))
def test_scroll_wheel(page: FirefoxPage, results: TestResult):
"""测试9: 滚轮滚动操作"""
try:
page.scroll.to_bottom()
page.wait(0.5)
page.actions.scroll(0, -500).perform()
page.wait(0.5)
results.record("scroll() 滚轮滚动", True, "滚动到底部+向上滚动")
except Exception as e:
results.record("scroll() 滚轮滚动", False, str(e))
def test_hover(page: FirefoxPage, results: TestResult):
"""测试10: 鼠标悬停操作"""
try:
page.scroll.to_top()
page.wait(0.3)
hover_target = page.ele("#hover-target")
page.actions.move_to(hover_target).perform()
page.wait(1)
result = page.ele("#hover-result").text
ok = "进入" in str(result)
results.record("move_to() 鼠标悬停", ok, f"结果: {result}")
except Exception as e:
results.record("move_to() 鼠标悬停", False, str(e))
def test_shift_click(page: FirefoxPage, results: TestResult):
"""测试11: Shift+点击组合"""
try:
click_btn = page.ele("#click-btn")
page.actions.key_down(Keys.SHIFT).click(click_btn).key_up(Keys.SHIFT).perform()
page.wait(0.3)
results.record("Shift+click 组合操作", True, "执行无异常")
except Exception as e:
results.record("Shift+click 组合操作", False, str(e))
def test_action_chain(page: FirefoxPage, results: TestResult):
"""测试12: 连续动作链"""
try:
click_btn = page.ele("#click-btn")
page.actions.move_to(click_btn).wait(0.3).click().wait(0.3).click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
results.record("连续动作链 (move+wait+click)", True, f"结果: {result}")
except Exception as e:
results.record("连续动作链 (move+wait+click)", False, str(e))
def test_release_all(page: FirefoxPage, results: TestResult):
"""测试13: release_all() 释放所有动作"""
try:
page.actions.release_all()
results.record("release_all() 释放所有动作", True, "执行无异常")
except Exception as e:
results.record("release_all() 释放所有动作", False, str(e))
def test_is_trusted_click(page: FirefoxPage, results: TestResult):
"""测试14: isTrusted 验证 - click"""
try:
page.ele("#click-btn").click_self()
page.wait(0.3)
trusted = page.is_trusted("click")
ok = trusted is True
results.record("isTrusted: click 事件", ok, f"isTrusted={trusted}")
except Exception as e:
results.record("isTrusted: click 事件", False, str(e))
def test_is_trusted_keydown(page: FirefoxPage, results: TestResult):
"""测试15: isTrusted 验证 - keydown"""
try:
page.ele("#text-input").input("K", clear=False)
page.wait(0.3)
trusted = page.is_trusted("keydown")
ok = trusted is True
results.record("isTrusted: keydown 事件", ok, f"isTrusted={trusted}")
except Exception as e:
results.record("isTrusted: keydown 事件", False, str(e))
def test_is_trusted_mouseenter(page: FirefoxPage, results: TestResult):
"""测试16: isTrusted 验证 - mouseenter"""
try:
hover_target = page.ele("#hover-target")
hover_target.hover()
page.wait(0.3)
trusted = page.is_trusted("mouseenter")
ok = trusted is True
results.record("isTrusted: mouseenter 事件", ok, f"isTrusted={trusted}")
except Exception as e:
results.record("isTrusted: mouseenter 事件", False, str(e))
def test_human_move_click(page: FirefoxPage, results: TestResult):
"""测试17: 拟人化移动+点击"""
try:
click_btn = page.ele("#click-btn")
page.actions.human_move(click_btn).human_click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
results.record("human_move + human_click 拟人点击", True, f"结果: {result}")
except Exception as e:
results.record("human_move + human_click 拟人点击", False, str(e))
def test_human_type(page: FirefoxPage, results: TestResult):
"""测试18: 拟人化输入"""
try:
input_elem = page.ele("#text-input")
input_elem.clear()
page.wait(0.2)
input_elem.click_self()
page.actions.human_type("拟人输入").perform()
page.wait(0.5)
val = input_elem.value
ok = "拟人输入" in str(val)
results.record("human_type() 拟人输入", ok, f"值: {val}")
except Exception as e:
results.record("human_type() 拟人输入", False, str(e))
def test_type_with_interval(page: FirefoxPage, results: TestResult):
"""测试19: type() 带间隔输入"""
try:
input_elem = page.ele("#text-input")
input_elem.clear()
page.wait(0.2)
input_elem.click_self()
page.actions.type("间隔输入", interval=50).perform()
page.wait(0.5)
val = input_elem.value
ok = "间隔输入" in str(val)
results.record("type(interval=50) 间隔输入", ok, f"值: {val}")
except Exception as e:
results.record("type(interval=50) 间隔输入", False, str(e))
def test_element_double_click(page: FirefoxPage, results: TestResult):
"""测试20: 元素级 double_click() 方法"""
try:
dbl_btn = page.ele("#double-click-btn")
dbl_btn.double_click()
page.wait(0.5)
result = page.ele("#click-result").text
ok = "双击" in str(result)
results.record("element.double_click() 元素双击", ok, f"结果: {result}")
except Exception as e:
results.record("element.double_click() 元素双击", False, str(e))
def test_element_right_click(page: FirefoxPage, results: TestResult):
"""测试21: 元素级 right_click() 方法"""
try:
right_btn = page.ele("#right-click-btn")
right_btn.right_click()
page.wait(0.5)
result = page.ele("#click-result").text
ok = "右键" in str(result)
results.record("element.right_click() 元素右击", ok, f"结果: {result}")
except Exception as e:
results.record("element.right_click() 元素右击", False, str(e))
def test_element_hover(page: FirefoxPage, results: TestResult):
"""测试22: 元素级 hover() 方法"""
try:
# 先移开鼠标
page.actions.move_to({"x": 0, "y": 0}).perform()
page.wait(0.3)
hover_target = page.ele("#hover-target")
hover_target.hover()
page.wait(0.5)
result = page.ele("#hover-result").text
ok = "进入" in str(result)
results.record("element.hover() 元素悬停", ok, f"结果: {result}")
except Exception as e:
results.record("element.hover() 元素悬停", False, str(e))
def test_element_drag_to(page: FirefoxPage, results: TestResult):
"""测试23: 元素级 drag_to() 方法"""
try:
start, end = _prepare_drag_scene(page)
if not start or not end:
results.record("element.drag_to() 元素拖拽", False, "无法获取元素坐标")
return
# 使用元素级 drag_to() 并传入坐标 dict
# 传入 dict 时,内部不会再 scrollIntoView,直接用当前视口坐标拖拽
draggable = page.ele("#draggable")
draggable.drag_to(end, duration=0.8)
page.wait(1)
result = page.ele("#drag-result").text
ok = "拖放成功" in str(result)
results.record("element.drag_to() 元素拖拽", ok, f"结果: {result}")
except Exception as e:
results.record("element.drag_to() 元素拖拽", False, str(e))
def test_file_upload(page: FirefoxPage, results: TestResult):
"""测试24: 文件上传 (input.setFiles)"""
try:
# 创建临时测试文件
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, prefix="ruyipage_test_"
)
tmp.write("RuyiPage file upload test")
tmp.close()
try:
file_input = page.ele("#file-input")
file_input.input(tmp.name)
page.wait(0.5)
# 验证文件名
val = page.run_js(
"return document.getElementById('file-input').files[0]?.name || ''"
)
ok = val and len(str(val)) > 0
results.record("input.setFiles 文件上传", ok, f"文件名: {val}")
finally:
os.unlink(tmp.name)
except Exception as e:
results.record("input.setFiles 文件上传", False, str(e))
def test_keyboard_navigation(page: FirefoxPage, results: TestResult):
"""测试25: 键盘导航 (Tab/Shift+Tab)"""
try:
text_input = page.ele("#text-input")
text_input.click_self()
page.wait(0.2)
# Tab 到下一个表单元素
page.actions.press(Keys.TAB).perform()
page.wait(0.3)
# 验证焦点转移
active_tag = page.run_js("return document.activeElement?.id || ''")
ok = active_tag and active_tag != "text-input"
results.record("Tab 键盘导航", ok, f"焦点在: {active_tag}")
except Exception as e:
results.record("Tab 键盘导航", False, str(e))
def test_relative_move(page: FirefoxPage, results: TestResult):
"""测试26: move() 相对移动"""
try:
click_btn = page.ele("#click-btn")
page.actions.move_to(click_btn).perform()
old_x = page.actions.curr_x
old_y = page.actions.curr_y
page.actions.move(50, 30).perform()
new_x = page.actions.curr_x
new_y = page.actions.curr_y
ok = (new_x == old_x + 50) and (new_y == old_y + 30)
results.record("move() 相对移动", ok, f"({old_x},{old_y}) → ({new_x},{new_y})")
except Exception as e:
results.record("move() 相对移动", False, str(e))
def test_backward_compat_db_click(page: FirefoxPage, results: TestResult):
"""测试27: 向后兼容 db_click() 别名"""
try:
dbl_btn = page.ele("#double-click-btn")
page.actions.db_click(dbl_btn).perform()
page.wait(0.5)
result = page.ele("#click-result").text
ok = "双击" in str(result)
results.record("db_click() 向后兼容别名", ok, f"结果: {result}")
except Exception as e:
results.record("db_click() 向后兼容别名", False, str(e))
def test_backward_compat_r_click(page: FirefoxPage, results: TestResult):
"""测试28: 向后兼容 r_click() 别名"""
try:
right_btn = page.ele("#right-click-btn")
page.actions.r_click(right_btn).perform()
page.wait(0.5)
result = page.ele("#click-result").text
ok = "右键" in str(result)
results.record("r_click() 向后兼容别名", ok, f"结果: {result}")
except Exception as e:
results.record("r_click() 向后兼容别名", False, str(e))
def test_multi_combo_keys(page: FirefoxPage, results: TestResult):
"""测试29: 多修饰键 combo (Ctrl+Shift+组合)"""
try:
input_elem = page.ele("#text-input")
input_elem.input("hello world", clear=True)
page.wait(0.2)
# Ctrl+A全选后再输入新内容
input_elem.click_self()
page.actions.combo(Keys.CTRL, "a").perform()
page.wait(0.1)
page.actions.type("replaced").perform()
page.wait(0.3)
val = input_elem.value
ok = str(val) == "replaced"
results.record("多步combo Ctrl+A → 输入替换", ok, f"值: {val}")
except Exception as e:
results.record("多步combo Ctrl+A → 输入替换", False, str(e))
def test_scroll_on_element(page: FirefoxPage, results: TestResult):
"""测试30: 在指定元素上滚动"""
try:
scroll_container = page.ele("#scroll-container")
page.actions.scroll(0, 300, on_ele=scroll_container).perform()
page.wait(0.5)
# 读取容器滚动位置
scroll_top = page.run_js(
"return document.getElementById('scroll-container').scrollTop"
)
ok = scroll_top and int(scroll_top) > 0
results.record("scroll(on_ele) 元素内滚动", ok, f"scrollTop={scroll_top}")
except Exception as e:
results.record("scroll(on_ele) 元素内滚动", False, str(e))
def test_touch_tap(page: FirefoxPage, results: TestResult):
"""测试31: touch.tap() 单指点击"""
try:
page.get(page.url)
page.wait(0.8)
click_btn = page.ele("#click-btn")
page.touch.tap(click_btn).perform()
page.wait(0.5)
result = page.ele("#click-result").text
ok = "点击次数" in str(result)
results.record("touch.tap() 单指点击", ok, f"结果: {result}")
except Exception as e:
results.record("touch.tap() 单指点击", False, str(e))
def test_touch_long_press(page: FirefoxPage, results: TestResult):
"""测试32: touch.long_press() 长按"""
try:
hover_target = page.ele("#hover-target")
page.touch.long_press(hover_target, duration=600).perform()
page.wait(0.4)
results.record("touch.long_press() 长按", True, "执行无异常")
except Exception as e:
results.record("touch.long_press() 长按", False, str(e))
# ═══════════════════════════════════════════════════════════════════
# 主测试入口
# ═══════════════════════════════════════════════════════════════════
def test_advanced_input():
"""运行所有高级输入操作测试"""
print("=" * 70)
print("测试20: 高级输入操作(综合测试)")
print("=" * 70)
opts = FirefoxOptions()
opts.headless(False)
page = FirefoxPage(opts)
results = TestResult()
try:
# 加载测试页面
test_page = os.path.join(
os.path.dirname(__file__), "test_pages", "test_page.html"
)
test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
page.get(test_url)
page.wait(1)
# ── 键盘操作测试 ──────────────────────────────────────
print("\n--- 键盘操作 ---")
test_combo_ctrl_a(page, results)
test_combo_copy_paste(page, results)
test_press_key(page, results)
# ── 鼠标点击测试 ──────────────────────────────────────
print("\n--- 鼠标点击操作 ---")
test_double_click(page, results)
test_right_click(page, results)
test_middle_click(page, results)
# ── 拖拽测试 ──────────────────────────────────────────
print("\n--- 拖拽操作 ---")
test_drag_hold_release(page, results)
test_actions_drag_to(page, results)
# ── 滚轮测试 ──────────────────────────────────────────
print("\n--- 滚轮操作 ---")
test_scroll_wheel(page, results)
test_scroll_on_element(page, results)
# ── 悬停测试 ──────────────────────────────────────────
print("\n--- 悬停操作 ---")
test_hover(page, results)
# ── 组合操作测试 ──────────────────────────────────────
print("\n--- 组合操作 ---")
test_shift_click(page, results)
test_action_chain(page, results)
test_release_all(page, results)
# ── isTrusted 验证 ────────────────────────────────────
print("\n--- isTrusted 验证 ---")
test_is_trusted_click(page, results)
test_is_trusted_keydown(page, results)
test_is_trusted_mouseenter(page, results)
# ── 拟人化操作 ────────────────────────────────────────
print("\n--- 拟人化操作 ---")
test_human_move_click(page, results)
test_human_type(page, results)
# ── 触摸操作 ──────────────────────────────────────────
print("\n--- 触摸操作 ---")
test_touch_tap(page, results)
test_touch_long_press(page, results)
# ── 输入操作 ──────────────────────────────────────────
print("\n--- 输入操作 ---")
test_type_with_interval(page, results)
# ── 元素级操作 ────────────────────────────────────────
print("\n--- 元素级操作 ---")
test_element_double_click(page, results)
test_element_right_click(page, results)
test_element_hover(page, results)
test_element_drag_to(page, results)
# ── 文件上传 ──────────────────────────────────────────
print("\n--- 文件上传 ---")
test_file_upload(page, results)
# ── 键盘导航 ──────────────────────────────────────────
print("\n--- 键盘导航 ---")
test_keyboard_navigation(page, results)
# ── 相对移动 ──────────────────────────────────────────
print("\n--- 其他操作 ---")
test_relative_move(page, results)
# ── 向后兼容性 ────────────────────────────────────────
print("\n--- 向后兼容性 ---")
test_backward_compat_db_click(page, results)
test_backward_compat_r_click(page, results)
# ── 多步组合 ──────────────────────────────────────────
print("\n--- 复杂多步操作 ---")
test_multi_combo_keys(page, results)
except Exception as e:
print(f"\n✗ 测试过程中发生意外错误: {e}")
traceback.print_exc()
finally:
# 输出汇总表格
all_passed = results.summary()
page.wait(2)
page.quit()
return all_passed
if __name__ == "__main__":
success = test_advanced_input()
sys.exit(0 if success else 1)
专注于高层原生 drag 能否正确触发真实 drop 事件。
源码文件:app/content/ruyipage_examples/35_native_bidi_drag.py · 仓库:35_native_bidi_drag.py
# =============================
# RuyiPage 示例注释版
# 标题: 原生 BiDi 拖拽:验证 HTML5 drop 与 trusted 鼠标事件
# 说明: 专注于高层原生 drag 能否正确触发真实 drop 事件。
# 你会学到:
# 1. 只用高层 API 实现拖拽
# 2. 验证 drop 是否命中目标
# 3. 统计 trusted move 次数和鼠标事件状态
# 相关 API: page.actions.drag, page.actions.release, page.run_js, page.ele
# =============================
# -*- coding: utf-8 -*-
"""示例35: 原生 BiDi 拖拽(严格结果版)"""
import io
import os
import sys
from typing import Dict, List
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
from ruyipage import FirefoxOptions, FirefoxPage
def add_result(
results: List[Dict[str, str]], item: str, status: str, note: str
) -> None:
results.append({"item": item, "status": status, "note": note})
def print_results(results: List[Dict[str, str]]) -> None:
print("\n| 项目 | 状态 | 说明 |")
print("| --- | --- | --- |")
for row in results:
print(f"| {row['item']} | {row['status']} | {row['note']} |")
def main() -> None:
print("=" * 70)
print("测试 35: 原生 BiDi 拖拽")
print("=" * 70)
opts = FirefoxOptions()
opts.headless(False)
page = FirefoxPage(opts)
results: List[Dict[str, str]] = []
try:
test_page = os.path.join(
os.path.dirname(__file__), "test_pages", "native_bidi_drag_test.html"
)
test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
page.get(test_url)
page.wait(0.8)
add_result(results, "测试页加载", "成功", test_url)
source = page.ele("#drag-source")
target = page.ele("#drop-target")
if not source or not target:
add_result(results, "拖拽元素定位", "失败", "未找到 source 或 target 元素")
print_results(results)
return
# 只使用高层原生 BiDi 拖拽能力,不直接暴露 _bidi.input_。
page.actions.drag(source, target, duration=640, steps=16).perform()
page.actions.release()
page.wait(0.8)
state = page.run_js("return window.nativeBidiDragState") or {}
result_text = page.ele("#result").text
if result_text == "拖拽成功":
add_result(results, "拖拽结果文本", "成功", result_text)
else:
add_result(results, "拖拽结果文本", "失败", result_text or "")
if state.get("dropped") is True:
add_result(results, "HTML5 drop 命中", "成功", "dropped=True")
else:
add_result(results, "HTML5 drop 命中", "失败", f"state={state}")
if state.get("enteredTarget") is True:
add_result(results, "目标区域进入", "成功", "enteredTarget=True")
else:
add_result(results, "目标区域进入", "失败", f"state={state}")
if (
state.get("trustedMouseDown") is True
and state.get("trustedMouseUp") is True
):
add_result(
results,
"isTrusted mouse down/up",
"成功",
"mouseDown/mouseUp 均为 trusted",
)
else:
add_result(
results,
"isTrusted mouse down/up",
"失败",
f"down={state.get('trustedMouseDown')} up={state.get('trustedMouseUp')}",
)
move_count = int(state.get("trustedMoveCount") or 0)
if move_count > 0:
add_result(
results,
"isTrusted move count",
"成功",
f"trustedMoveCount={move_count}",
)
else:
add_result(
results,
"isTrusted move count",
"失败",
f"trustedMoveCount={move_count}",
)
last_x = state.get("lastClientX")
last_y = state.get("lastClientY")
if last_x is not None and last_y is not None:
add_result(results, "最后指针坐标", "成功", f"({last_x}, {last_y})")
else:
add_result(results, "最后指针坐标", "跳过", "页面未记录最后指针坐标")
print_results(results)
except Exception as e:
add_result(results, "示例执行", "失败", str(e)[:160])
print_results(results)
raise
finally:
try:
page.actions.release()
except Exception:
pass
try:
page.quit()
except Exception:
pass
if __name__ == "__main__":
main()
通过普通 JS 事件与带 ruyi=true 的扩展事件做对照,帮助你理解为什么前端 dispatchEvent 默认不是 trusted 行为。
用于排查自动化脚本里“鼠标到底移到了哪里、点击是否命中、JS click 与原生动作有什么区别”的可视化诊断示例。