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

RuyiPage isTrusted

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(...))falseJS 派发,仅用于测试

核心 API

page.actions(所有方法)

返回: Actions
page.actions.click / type / human_click / human_type / combo / ...

所有动作链方法产生的浏览器事件 isTrusted=true,等价于用户真实操作。

常见坑
  • 不要用 JS dispatchEvent 替代 actions,高风控页面会识别。
相关示例

快速上手

python 验证 isTrusted 事件
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()

相关示例源码

5 个示例 ▶ 点击展开

动作链:原生点击、键盘、滚轮、拖拽与 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)

原生 BiDi 拖拽:验证 HTML5 drop 与 trusted 鼠标事件

examples/35_native_bidi_drag.py

专注于高层原生 drag 能否正确触发真实 drop 事件。

GitHub 仓库 查看仓库源码 page.actions.dragpage.actions.releasepage.run_jspage.ele
  • 只用高层 API 实现拖拽
  • 验证 drop 是否命中目标
  • 统计 trusted move 次数和鼠标事件状态

源码文件:app/content/ruyipage_examples/35_native_bidi_drag.py · 仓库:35_native_bidi_drag.py

python 注释版
# =============================
# 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 setter 与事件派发:对照 isTrusted 与 untrusted 行为

examples/45_js_setter_untrusted_input.py

通过普通 JS 事件与带 ruyi=true 的扩展事件做对照,帮助你理解为什么前端 dispatchEvent 默认不是 trusted 行为。

GitHub 仓库 查看仓库源码 page.run_jspage.getpage.wait.doc_loadednew Event(...)new InputEvent(...)new KeyboardEvent(...)new MouseEvent(...)isTrusted
  • 对比普通 JS 事件和 ruyi=true 扩展事件的 isTrusted 差异
  • 验证 input、keydown、click、focus、pointer、wheel 等事件类型
  • 理解为什么 dispatchEvent 不能等价替代原生动作链
  • 为高风控场景排查前端事件信任问题

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

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 的差异
  • 排查拖拽、点击、输入命中问题