全球动态代理 · 静态住宅代理 · 极高质量 · 包月不限量 · 学员低价 · 联系如意进行免费测试 · 欢迎公司及学员采购 · 另有任何海外数据采集需求,随时联系 · 商务合作微信:Charleval
Automation / 动作链
RuyiPage Docs

RuyiPage 动作链

page.actions 是原生动作链入口,产生 isTrusted=true 的真实鼠标和键盘事件。支持点击、输入、组合键、拖拽和拟人化操作。

核心 API

page.actions.click

返回: Actions(支持链式调用)
page.actions.click(target=None)

原生鼠标点击。先 move_to 再 click 更稳定。

参数说明
参数说明
target元素或坐标,不传则点当前鼠标位置。
常见坑
  • 先 move_to 再 click 命中率更高。
相关示例

page.actions.press

返回: Actions
page.actions.press(key)

按键操作,常用于 Enter、Tab、Esc 等功能键。

参数说明
参数说明
keyKeys 枚举或按键字符串。
常见坑
  • 元素未获得焦点时按键可能发给错误目标。
相关示例

page.actions.type

返回: Actions
page.actions.type(text)

原生键盘输入文本。每个字符产生真实按键事件。

参数说明
参数说明
text要输入的文本。
常见坑
  • 大段文本输入较慢,追求速度时用 ele.input()。
相关示例

其他动作方法

API说明
page.actions.move_to(ele_or_xy)移动鼠标到元素或坐标
page.actions.db_click(target)双击
page.actions.r_click(target)右键点击
page.actions.hold(ele_or_xy)按住鼠标不放(拖拽起点)
page.actions.release(ele_or_xy)释放鼠标
page.actions.release_all()释放所有动作状态
page.actions.human_click(target)拟人化点击
page.actions.human_move(target)拟人化鼠标移动轨迹
page.actions.scroll(dx, dy)鼠标滚轮滚动
page.actions.key_down(key) / key_up(key)按下/释放单个按键

快速上手

python 动作链基础操作
from ruyipage import launch, Keys

page = launch()
page.get("https://cn.bing.com/")

# 点击搜索框 → 输入 → 按 Enter
page.actions.move_to(page.ele("#sb_form_q")).click().perform()
page.actions.type("RuyiPage").perform()
page.actions.press(Keys.ENTER).perform()
page.wait(3)

# 组合键:Ctrl+A 全选
page.actions.combo(Keys.CONTROL, "a").perform()

page.quit()

拟人化输入示例

python 高风控场景推荐写法
# 拟人化操作:随机延迟,模拟真人
page.actions.human_move(page.ele("#input-box")).perform()
page.actions.human_click().perform()
page.actions.human_type("hello world").perform()

相关示例源码

3 个示例 ▶ 点击展开

动作链:原生点击、键盘、滚轮、拖拽与 isTrusted

examples/05_actions_chain.py

强调 BiDi 原生动作链的事件真实性,是理解 isTrusted 的关键示例。

GitHub 仓库 查看仓库源码 page.actions.move_toclickdb_clickr_clicktypekey_downkey_upscroll
  • 移动到元素后执行点击、双击和右击
  • 使用动作链输入文本
  • 执行组合键如 Ctrl+A / Ctrl+C / Ctrl+V
  • 执行页面滚轮滚动
  • 执行拖拽并验证 trusted 事件
  • 释放动作状态避免后续污染

源码文件:app/content/ruyipage_examples/05_actions_chain.py · 仓库:05_actions_chain.py

python 注释版
# =============================
# 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()

高级输入操作:组合键、拟人输入、拖拽、触摸、上传、键盘导航

examples/20_advanced_input.py

输入能力大全示例,是 actions、touch、uploads 和 isTrusted 的主参考。

GitHub 仓库 查看仓库源码 page.actions.combopage.actions.presspage.actions.double_clickpage.actions.right_clickpage.actions.middle_clickpage.actions.dragpage.actions.drag_topage.actions.scroll
  • 组合键与复制粘贴
  • 双击、右击、中键点击
  • 拖拽和元素级 drag_to
  • 滚轮和 hover
  • 拟人化移动、点击和输入
  • 触摸点击与长按
  • 文件上传和键盘导航

源码文件:app/content/ruyipage_examples/20_advanced_input.py · 仓库:20_advanced_input.py

python 注释版
# =============================
# 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)

动作可视化:显示鼠标轨迹、命中点与目标元素覆盖层

examples/42_2_action_visual_showcase.py

用于排查自动化脚本里“鼠标到底移到了哪里、点击是否命中、JS click 与原生动作有什么区别”的可视化诊断示例。

GitHub 仓库 查看仓库源码 launch(action_visual=True)page.actions.move_topage.actions.clickpage.actions.human_movepage.actions.drag_toele.click.by_jsele.input(..., by_js=True)
  • 显示 BiDi 鼠标移动轨迹
  • 显示点击位置高亮与闪烁提示
  • 显示当前目标元素覆盖层
  • 对比原生动作和 JS click / JS input 的差异
  • 排查拖拽、点击、输入命中问题