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

RuyiPage 示例中心

这里不是简单的文件清单,而是将 RuyiPage 仓库中的 examples 渲染成案例卡。每个例子都带有文件名、分类、难度、不同用法、涉及 API、适用场景与相关页面,并提供 GitHub 源码入口,帮助你从“例子”回到“文档”,也从“文档”回到“例子”。

Cloudflare / Copilot 快速开始

quickstart_cloudfare.py
GitHub 仓库 已同步源码 查看 GitHub 源码
emulation advanced
一句话定位

面向高风控站点的快速开始示例,覆盖挑战处理、输入框识别和 Cookie 提取。

涉及 API
FirefoxOptionsFirefoxPagepage.getpage.elepage.handle_cloudflare_challengepage.get_cookies
展示的不同用法
  • 访问高风控站点并等待页面完成首屏加载
  • 自动尝试处理 Cloudflare 挑战
  • 定位输入框并发送问题
  • 提取 document.cookie 与完整 Cookie 信息
适用场景

适合 anti-bot、Firefox 指纹环境说明和真实站点实战章节。

推荐阅读
anti-botemulation-21advanced-input-20
完整代码

来源:app/content/ruyipage_examples/quickstart_cloudfare.py · 仓库文件:quickstart_cloudfare.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: Cloudflare / Copilot 快速开始
# 说明: 面向高风控站点的快速开始示例,覆盖挑战处理、输入框识别和 Cookie 提取。
# 你会学到:
#   1. 访问高风控站点并等待页面完成首屏加载
#   2. 自动尝试处理 Cloudflare 挑战
#   3. 定位输入框并发送问题
#   4. 提取 document.cookie 与完整 Cookie 信息
# 相关 API: FirefoxOptions, FirefoxPage, page.get, page.ele, page.handle_cloudflare_challenge, page.get_cookies
# =============================

# -*- coding: utf-8 -*-
"""快速开始:访问 Copilot,自动尝试通过 Cloudflare,并打印完整 Cookie。"""

from ruyipage import FirefoxOptions, FirefoxPage, Keys


QUESTION = "你好,今天天气怎么样?"


def find_input_box(page: FirefoxPage):
    for _ in range(30):
        box = page.ele("css:textarea")
        if not box:
            box = page.ele('css:[contenteditable="true"]')
        if not box:
            box = page.ele("css:.input-area")
        if box:
            return box
        page.wait(1)
    return None


def print_full_cookies(page: FirefoxPage) -> None:
    print("\n" + "=" * 60)
    print("Cloudflare / 页面 Cookie")
    print("=" * 60)

    raw_cookie = page.run_js("return document.cookie") or ""
    print(f"document.cookie: {raw_cookie}")

    cookies = page.get_cookies(all_info=True)
    print(f"Cookie 数量: {len(cookies)}")
    for i, cookie in enumerate(cookies, 1):
        print(f"[{i}] name={cookie.name}")
        print(f"    value={cookie.value}")
        print(f"    domain={cookie.domain}")
        print(f"    path={cookie.path}")
        print(f"    httpOnly={cookie.http_only}")
        print(f"    secure={cookie.secure}")
        print(f"    sameSite={cookie.same_site}")
        print(f"    expiry={cookie.expiry}")


opts = FirefoxOptions()
# 如果 Firefox 不在默认安装目录,可以取消注释并指定路径。
# opts.set_browser_path(r"D:\Firefox\firefox.exe")

# 如果你想复用登录状态、Cookie、扩展,可以取消注释并指定 userdir。
# opts.set_user_dir(r"D:\ruyipage_userdir")

page = FirefoxPage(opts)

try:
    print("=" * 60)
    print("copilot.microsoft.com Cloudflare 测试")
    print("=" * 60)

    print("\n-> 访问 https://copilot.microsoft.com/ ...")
    page.get("https://copilot.microsoft.com/", wait="none")
    page.wait(5)

    print("-> 等待输入框...")
    input_box = find_input_box(page)

    if input_box:
        print("-> 找到输入框,开始输入问题...")
        try:
            input_box.click()
            page.wait(0.8)
            input_box.input(QUESTION, clear=True)
            page.wait(0.8)

            send_btn = page.ele('css:button[aria-label*="Send"]')
            if not send_btn:
                send_btn = page.ele('css:button[type="submit"]')

            if send_btn:
                print("-> 点击发送按钮...")
                send_btn.click()
            else:
                print("-> 按 Enter 发送...")
                page.actions.press(Keys.ENTER).perform()

            print("-> 已发送问题,等待 Cloudflare 触发...")
            page.wait(15)
        except Exception as e:
            print(f"-> 发送失败: {e}")
            page.wait(5)
    else:
        print("-> 未找到输入框,直接等待 Cloudflare...")
        page.wait(5)

    print("\n-> 开始自动处理 Cloudflare...")
    passed = page.handle_cloudflare_challenge(timeout=120, check_interval=2)

    print("\n" + "=" * 60)
    if passed:
        print("✅ 成功通过 Cloudflare!")
        print_full_cookies(page)
    else:
        print("❌ 超时未通过")

    print("\n[+] 保持浏览器打开 500 秒...")
    page.wait(500)

finally:
    page.quit()

指纹浏览器快速开始

quickstart_fingerprint_browser.py
GitHub 仓库 待同步 查看 GitHub 源码
browser advanced
一句话定位

演示如何把 Firefox 指纹浏览器、指纹文件、地理位置、语言和请求头一起接入 RuyiPage。

涉及 API
FirefoxOptionsopts.set_browser_pathopts.set_fpfilepage.emulation.set_geolocationpage.emulation.set_timezonepage.emulation.set_localepage.network.set_extra_headerspage.emulation.set_screen_size
展示的不同用法
  • 加载 Firefox 指纹浏览器可执行文件
  • 通过 fpfile 复用指纹配置
  • 叠加地理位置、时区和语言环境
  • 为请求增加语言请求头
  • 访问指纹检测站点验证效果
适用场景

适合高风控、跨地区语言环境模拟和指纹浏览器接入文档。

推荐阅读
quickstart-cloudfareemulation-21advanced-network-18

隐私模式快速开始

quickstart_private_mode.py
GitHub 仓库 待同步 查看 GitHub 源码
browser beginner
一句话定位

展示 Firefox 私密浏览模式的两种启动方式,以及它与临时 profile 的区别。

涉及 API
FirefoxOptionsopts.private_modeFirefoxPagelaunch(private=True)page.getpage.quit
展示的不同用法
  • 通过 FirefoxOptions 开启私密浏览
  • 通过 launch(private=True) 快速启动
  • 理解私密模式与临时用户目录的区别
  • 验证私密模式下的页面访问流程
适用场景

适合新手理解浏览器启动模式、临时会话和隐私场景。

推荐阅读
quickstart-01basic-navigation-01

一步上手:最少代码完成访问与读取

examples/01_quickstart.py
GitHub 仓库 已同步源码 查看 GitHub 源码
starter beginner
一句话定位

最短可运行的 hello-world 级别示例,适合验证环境和建立第一层心智模型。

涉及 API
launchpage.getpage.waitpage.titlepage.eleele.textpage.quit
展示的不同用法
  • 用 launch() 启动 Firefox
  • 访问网页并等待页面稳定
  • 读取标题和 H1 文本
  • 演示最短退出流程
适用场景

适合安装页后的第一段示例代码。

相关文档页面
推荐阅读
basic-navigation-01quickstart-bing-search
完整代码

来源:app/content/ruyipage_examples/01_quickstart.py · 仓库文件:01_quickstart.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 一步上手:最少代码完成访问与读取
# 说明: 最短可运行的 hello-world 级别示例,适合验证环境和建立第一层心智模型。
# 你会学到:
#   1. 用 launch() 启动 Firefox
#   2. 访问网页并等待页面稳定
#   3. 读取标题和 H1 文本
#   4. 演示最短退出流程
# 相关 API: launch, page.get, page.wait, page.title, page.ele, ele.text, page.quit
# =============================

# -*- coding: utf-8 -*-
"""
示例01: 一步上手

面向新手:最少代码完成一次页面访问与元素交互。
"""

import io
import sys


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 launch


def quickstart_demo():
    print("=" * 60)
    print("示例01: 一步上手")
    print("=" * 60)

    page = launch(headless=False)

    try:
        page.get("https://example.com")
        page.wait(1)
        print(f"标题: {page.title}")

        h1 = page.ele("tag:h1")
        print(f"H1文本: {h1.text}")

        print("\n✓ 快速上手完成")
    finally:
        page.wait(1)
        page.quit()


if __name__ == "__main__":
    quickstart_demo()

基础导航与页面保存

examples/01_basic_navigation.py
GitHub 仓库 已同步源码 查看 GitHub 源码
starter beginner
一句话定位

覆盖最基础的页面访问、前进后退和保存 HTML/PDF。

涉及 API
FirefoxPageFirefoxOptionspage.getpage.titlepage.urlpage.htmlpage.backpage.forwardpage.refreshpage.save
展示的不同用法
  • 打开本地文件页面和外部页面
  • 读取标题、URL 和 HTML 源码
  • 执行前进、后退和刷新
  • 保存 HTML 与 PDF
适用场景

适合 page、navigation、pdf 页面共同引用。

相关文档页面
推荐阅读
quickstart-01tabs-09pdf-19
完整代码

来源:app/content/ruyipage_examples/01_basic_navigation.py · 仓库文件:01_basic_navigation.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 基础导航与页面保存
# 说明: 覆盖最基础的页面访问、前进后退和保存 HTML/PDF。
# 你会学到:
#   1. 打开本地文件页面和外部页面
#   2. 读取标题、URL 和 HTML 源码
#   3. 执行前进、后退和刷新
#   4. 保存 HTML 与 PDF
# 相关 API: FirefoxPage, FirefoxOptions, page.get, page.title, page.url, page.html, page.back, page.forward
# =============================

# -*- coding: utf-8 -*-
"""
示例1: 基础导航和页面操作
测试功能:
- 创建浏览器实例
- 页面导航
- 获取标题、URL
- 前进、后退、刷新
- 页面保存
"""

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

def test_basic_navigation():
    """测试基础导航功能"""
    print("=" * 60)
    print("测试1: 基础导航和页面操作")
    print("=" * 60)

    # 创建浏览器实例
    opts = FirefoxOptions()
    opts.headless(False)  # 显示浏览器窗口
    page = FirefoxPage(opts)

    try:
        # 1. 导航到测试页面
        test_page = os.path.join(os.path.dirname(__file__), 'test_pages', 'test_page.html')
        test_url = 'file:///' + os.path.abspath(test_page).replace('\\', '/')
        print(f"\n1. 导航到测试页面: {test_url}")
        page.get(test_url)
        print(f"   ✓ 页面加载成功")

        # 2. 获取页面信息
        print(f"\n2. 获取页面信息:")
        print(f"   标题: {page.title}")
        print(f"   URL: {page.url}")
        print(f"   HTML长度: {len(page.html)} 字符")

        # 3. 导航到其他页面
        print(f"\n3. 导航到其他页面:")
        page.get('https://www.example.com')
        print(f"   当前标题: {page.title}")
        print(f"   当前URL: {page.url}")

        # 4. 后退
        print(f"\n4. 后退到上一页:")
        page.back()
        page.wait(1)
        print(f"   当前标题: {page.title}")

        # 5. 前进
        print(f"\n5. 前进到下一页:")
        page.forward()
        page.wait(1)
        print(f"   当前标题: {page.title}")

        # 6. 刷新页面
        print(f"\n6. 刷新页面:")
        page.refresh()
        page.wait(1)
        print(f"   ✓ 页面已刷新")

        # 7. 保存页面
        print(f"\n7. 保存页面:")
        save_dir = os.path.join(os.path.dirname(__file__), 'output')
        os.makedirs(save_dir, exist_ok=True)

        # 保存为HTML
        html_path = page.save(path=save_dir, name='example_page', as_pdf=False)
        print(f"   HTML已保存: {html_path}")

        # 保存为PDF
        pdf_path = page.save(path=save_dir, name='example_page', as_pdf=True)
        print(f"   PDF已保存: {pdf_path}")

        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_basic_navigation()

元素查找:CSS / XPath / 文本 / 属性 / 容器内定位

examples/02_element_finding.py
GitHub 仓库 已同步源码 查看 GitHub 源码
elements beginner
一句话定位

系统讲透单元素、多元素、文本、XPath 和属性定位的入门示例。

涉及 API
page.elepage.elesele.eleele.textele.attrele.attrsele.tag
展示的不同用法
  • 通过 ID、class 和属性定位元素
  • 批量获取列表元素
  • 通过 XPath 和文本定位元素
  • 在父容器内继续查找子元素
  • 读取元素标签、属性和尺寸信息
适用场景

适合 selectors 与 elements 文档页。

相关文档页面
推荐阅读
elements-03iframe-13
完整代码

来源:app/content/ruyipage_examples/02_element_finding.py · 仓库文件:02_element_finding.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 元素查找:CSS / XPath / 文本 / 属性 / 容器内定位
# 说明: 系统讲透单元素、多元素、文本、XPath 和属性定位的入门示例。
# 你会学到:
#   1. 通过 ID、class 和属性定位元素
#   2. 批量获取列表元素
#   3. 通过 XPath 和文本定位元素
#   4. 在父容器内继续查找子元素
#   5. 读取元素标签、属性和尺寸信息
# 相关 API: page.ele, page.eles, ele.ele, ele.text, ele.attr, ele.attrs, ele.tag
# =============================

# -*- coding: utf-8 -*-
"""
示例2: 元素查找
测试功能:
- CSS选择器查找
- XPath查找
- 文本查找
- 属性查找
- 多元素查找
- 元素属性获取
"""

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

def test_element_finding():
    """测试元素查找功能"""
    print("=" * 60)
    print("测试2: 元素查找")
    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. 通过ID查找
        print("\n1. 通过ID查找元素:")
        title = page.ele('#main-title')
        print(f"   标题文本: {title.text}")
        print(f"   标签名: {title.tag}")

        # 2. 通过class查找
        print("\n2. 通过class查找元素:")
        test_div = page.ele('.test-class')
        print(f"   第一个.test-class元素: {test_div.text}")

        # 3. 查找所有匹配元素
        print("\n3. 查找所有.test-class元素:")
        all_test = page.eles('.test-class')
        print(f"   找到 {len(all_test)} 个元素")
        for i, elem in enumerate(all_test, 1):
            print(f"   元素{i}: {elem.text}")

        # 4. 通过XPath查找
        print("\n4. 通过XPath查找:")
        link = page.ele('xpath://a[@id="test-link"]')
        print(f"   链接文本: {link.text}")
        print(f"   链接地址: {link.link}")

        # 5. 通过文本查找
        print("\n5. 通过文本查找:")
        btn = page.ele('text:点击我')
        print(f"   按钮ID: {btn.attr('id')}")

        # 6. 通过属性查找
        print("\n6. 通过属性查找:")
        test_div = page.ele('[data-test="value"]')
        print(f"   data-test属性的元素: {test_div.text}")
        print(f"   所有属性: {test_div.attrs}")

        # 7. 组合选择器
        print("\n7. 组合选择器:")
        section = page.ele('#form-section')
        input_elem = section.ele('input[type="text"]')
        print(f"   表单区域内的文本输入框: {input_elem.attr('placeholder')}")

        # 8. 获取元素属性
        print("\n8. 获取元素属性:")
        img = page.ele('#test-img')
        print(f"   图片alt: {img.attr('alt')}")
        print(f"   图片src: {img.src}")

        # 9. 元素状态检查
        print("\n9. 元素状态检查:")
        disabled_btn = page.ele('#disabled-btn')
        print(f"   禁用按钮是否可用: {disabled_btn.is_enabled}")
        print(f"   禁用按钮是否显示: {disabled_btn.is_displayed}")

        # 10. 元素尺寸和位置
        print("\n10. 元素尺寸和位置:")
        click_btn = page.ele('#click-btn')
        print(f"   按钮尺寸: {click_btn.size}")
        print(f"   按钮位置: {click_btn.location}")

        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_element_finding()

元素交互:点击、输入、选择、悬停、表单提交

examples/03_element_interaction.py
GitHub 仓库 已同步源码 查看 GitHub 源码
elements beginner
一句话定位

围绕元素对象的高层交互,覆盖输入、点击、选择和状态读取。

涉及 API
click_selfinputclearvalueselect.by_valuedouble_clickright_clickhover
展示的不同用法
  • 点击按钮并读取页面反馈
  • 填写文本、邮箱、密码和数字输入框
  • 清空后重新输入
  • 切换复选框和单选框
  • 在原生模式与兼容模式下操作 select
  • 执行双击、右击和悬停
适用场景

适合 forms、elements 和 actions 的过渡章节。

相关文档页面
推荐阅读
elements-02advanced-input-20
完整代码

来源:app/content/ruyipage_examples/03_element_interaction.py · 仓库文件:03_element_interaction.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 元素交互:点击、输入、选择、悬停、表单提交
# 说明: 围绕元素对象的高层交互,覆盖输入、点击、选择和状态读取。
# 你会学到:
#   1. 点击按钮并读取页面反馈
#   2. 填写文本、邮箱、密码和数字输入框
#   3. 清空后重新输入
#   4. 切换复选框和单选框
#   5. 在原生模式与兼容模式下操作 select
# 相关 API: click_self, input, clear, value, select.by_value, double_click, right_click, hover
# =============================

# -*- coding: utf-8 -*-
"""
示例3: 元素交互
测试功能:
- 点击元素
- 输入文本
- 清空输入
- 双击、右键点击
- 鼠标悬停
- 拖拽
"""

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


def test_element_interaction():
    """测试元素交互功能"""
    print("=" * 60)
    print("测试3: 元素交互")
    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")
        click_btn.click_self()
        page.wait(0.5)
        result = page.ele("#click-result").text
        print(f"   点击结果: {result}")

        # 再点击几次
        click_btn.click_self()
        click_btn.click_self()
        page.wait(0.5)
        result = page.ele("#click-result").text
        print(f"   多次点击后: {result}")

        # 2. 输入文本
        print("\n2. 输入文本:")
        text_input = page.ele("#text-input")
        text_input.input("Hello RuyiPage!")
        page.wait(0.5)
        print(f"   输入的值: {text_input.value}")

        # 3. 清空并重新输入
        print("\n3. 清空并重新输入:")
        text_input.clear()
        text_input.input("新的文本内容")
        print(f"   新的值: {text_input.value}")

        # 4. 输入到其他类型的输入框
        print("\n4. 输入到不同类型的输入框:")
        page.ele("#email-input").input("[email protected]")
        page.ele("#password-input").input("password123")
        page.ele("#number-input").input("42")
        page.ele("#textarea").input("这是多行文本\n第二行\n第三行")
        print(f"   ✓ 所有输入框填写完成")

        # 5. 复选框操作
        print("\n5. 复选框操作:")
        checkbox1 = page.ele("#checkbox1")
        checkbox2 = page.ele("#checkbox2")
        print(f"   checkbox1初始状态: {checkbox1.is_checked}")
        checkbox1.click_self()
        page.wait(0.3)
        print(f"   checkbox1点击后: {checkbox1.is_checked}")
        checkbox2.click_self()
        page.wait(0.3)
        print(f"   checkbox2点击后: {checkbox2.is_checked}")

        # 6. 单选框操作
        print("\n6. 单选框操作:")
        radio1 = page.ele("#radio1")
        radio2 = page.ele("#radio2")
        radio1.click_self()
        page.wait(0.3)
        print(f"   radio1选中: {radio1.is_checked}")
        radio2.click_self()
        page.wait(0.3)
        print(f"   radio2选中: {radio2.is_checked}")
        print(f"   radio1现在: {radio1.is_checked}")

        # 7. 下拉选择
        print("\n7. 下拉选择:")
        select = page.ele("#select-single")
        # 教学建议:优先 native_only,失败再切 compat。
        select_ok = select.select.by_value("opt2", mode="native_only")
        if not select_ok:
            print("   native_only 失败,切换到 compat 模式保底...")
            select_ok = select.select.by_value("opt2", mode="compat")
        page.wait(0.5)
        print(f"   选中的值: {select.value}")
        selected = select.select.selected_option
        print(f"   选中的文本: {selected['text']}")
        print(f"   原生选择结果: {select_ok}")

        # 8. 双击
        print("\n8. 双击按钮:")
        dbl_btn = page.ele("#double-click-btn")
        dbl_btn.double_click()
        page.wait(0.5)
        result = page.ele("#click-result").text
        print(f"   双击结果: {result}")

        # 9. 右键点击
        print("\n9. 右键点击:")
        right_btn = page.ele("#right-click-btn")
        right_btn.right_click()
        page.wait(0.5)
        result = page.ele("#click-result").text
        print(f"   右键点击结果: {result}")

        # 10. 鼠标悬停
        print("\n10. 鼠标悬停:")
        hover_target = page.ele("#hover-target")
        hover_target.hover()
        page.wait(0.5)
        hover_result = page.ele("#hover-result").text
        print(f"   悬停结果: {hover_result}")

        # 11. 提交表单
        print("\n11. 提交表单:")
        submit_btn = page.ele("#submit-btn")
        submit_btn.click_self()
        page.wait(0.5)
        form_result = page.ele("#form-result").text
        print(f"   表单结果: {form_result[:50]}...")

        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_element_interaction()

等待条件:出现、显示、隐藏、删除与页面就绪

examples/04_wait_conditions.py
GitHub 仓库 已同步源码 查看 GitHub 源码
elements beginner
一句话定位

用于建立稳定脚本的等待心智,比硬编码 sleep 更适合文档新手章节。

涉及 API
page.wait.elepage.wait.ele_displayedpage.wait.ele_hiddenpage.wait.ele_deletedpage.wait.title_containspage.wait.doc_loaded
展示的不同用法
  • 等待元素出现
  • 等待元素可见
  • 等待异步内容显示
  • 等待元素隐藏或被删除
  • 等待标题变化或文档加载完成
适用场景

适合 waits 页面,也常作为动态页面调试的前置阅读。

相关文档页面
推荐阅读
scrolling-10comprehensive-15
完整代码

来源:app/content/ruyipage_examples/04_wait_conditions.py · 仓库文件:04_wait_conditions.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 等待条件:出现、显示、隐藏、删除与页面就绪
# 说明: 用于建立稳定脚本的等待心智,比硬编码 sleep 更适合文档新手章节。
# 你会学到:
#   1. 等待元素出现
#   2. 等待元素可见
#   3. 等待异步内容显示
#   4. 等待元素隐藏或被删除
#   5. 等待标题变化或文档加载完成
# 相关 API: page.wait.ele, page.wait.ele_displayed, page.wait.ele_hidden, page.wait.ele_deleted, page.wait.title_contains, page.wait.doc_loaded
# =============================

# -*- coding: utf-8 -*-
"""
示例4: 等待条件
测试功能:
- 等待元素出现
- 等待元素可见
- 等待元素隐藏
- 等待元素删除
- 等待标题变化
- 等待URL变化
"""

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

def test_wait_conditions():
    """测试等待条件功能"""
    print("=" * 60)
    print("测试4: 等待条件")
    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. 等待元素出现:")
        elem = page.wait.ele('#main-title', timeout=5)
        if elem:
            print(f"   ✓ 元素已找到: {elem.text}")
        else:
            print(f"   ✗ 元素未找到")

        # 2. 等待元素可见
        print("\n2. 等待元素可见:")
        btn = page.wait.ele_displayed('#click-btn', timeout=5)
        if btn:
            print(f"   ✓ 按钮可见")
        else:
            print(f"   ✗ 按钮不可见")

        # 3. 触发延迟显示内容
        print("\n3. 等待延迟显示的内容:")
        show_btn = page.ele('text:显示延迟内容')
        show_btn.click_self()
        print(f"   已点击按钮,等待内容显示...")

        # 等待动态内容出现
        dynamic_elem = page.wait.ele_displayed('#dynamic-content', timeout=5)
        if dynamic_elem:
            print(f"   ✓ 延迟内容已显示: {dynamic_elem.text}")
        else:
            print(f"   ✗ 延迟内容未显示")

        # 4. 等待元素隐藏
        print("\n4. 等待元素隐藏:")
        hide_btn = page.ele('text:隐藏内容')
        hide_btn.click_self()
        print(f"   已点击隐藏按钮...")

        is_hidden = page.wait.ele_hidden('#dynamic-content', timeout=3)
        if is_hidden:
            print(f"   ✓ 元素已隐藏")
        else:
            print(f"   ✗ 元素仍然可见")

        # 5. 等待元素删除
        print("\n5. 等待元素从DOM删除:")
        remove_btn = page.ele('text:删除内容')
        remove_btn.click_self()
        print(f"   已点击删除按钮...")

        is_deleted = page.wait.ele_deleted('#dynamic-content', timeout=3)
        if is_deleted:
            print(f"   ✓ 元素已从DOM删除")
        else:
            print(f"   ✗ 元素仍在DOM中")

        # 6. 等待标题包含特定文本
        print("\n6. 等待标题包含特定文本:")
        current_title = page.title
        print(f"   当前标题: {current_title}")
        result = page.wait.title_contains('RuyiPage', timeout=2)
        if result:
            print(f"   ✓ 标题包含'RuyiPage'")
        else:
            print(f"   ✗ 标题不包含'RuyiPage'")

        # 7. 简单等待(暂停)
        print("\n7. 简单等待2秒:")
        page.wait(2)
        print(f"   ✓ 等待完成")

        # 8. 等待页面加载完成
        print("\n8. 等待页面加载完成:")
        page.wait.doc_loaded(timeout=5)
        print(f"   ✓ 页面已加载完成")

        # 9. 添加新内容并等待
        print("\n9. 添加新内容并等待:")
        add_btn = page.ele('text:添加内容')
        add_btn.click_self()
        page.wait(0.5)

        # 等待新内容出现
        new_content = page.wait.ele('#content-container .result', timeout=3)
        if new_content:
            print(f"   ✓ 新内容已添加: {new_content.text}")
        else:
            print(f"   ✗ 新内容未找到")

        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_wait_conditions()

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

examples/05_actions_chain.py
GitHub 仓库 已同步源码 查看 GitHub 源码
actions intermediate
一句话定位

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

涉及 API
page.actions.move_toclickdb_clickr_clicktypekey_downkey_upscrollholdreleaserelease_all
展示的不同用法
  • 移动到元素后执行点击、双击和右击
  • 使用动作链输入文本
  • 执行组合键如 Ctrl+A / Ctrl+C / Ctrl+V
  • 执行页面滚轮滚动
  • 执行拖拽并验证 trusted 事件
  • 释放动作状态避免后续污染
适用场景

适合 actions、is-trusted 和 drag-drop 页面。

相关文档页面
推荐阅读
advanced-input-20native-drag-35
完整代码

来源: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()

截图功能:整页、元素、base64、bytes

examples/06_screenshot.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser intermediate
一句话定位

覆盖最常用的页面与元素截图输出能力。

涉及 API
page.screenshotele.screenshotpage.scroll.to_seepage.scroll.to_bottom
展示的不同用法
  • 整页截图并保存到文件
  • 对单个元素或区域截图
  • 滚动到目标后截图
  • 获取截图的 base64 和 bytes 数据
适用场景

适合 screenshots 页面和综合示例的关联阅读。

相关文档页面
推荐阅读
pdf-19comprehensive-15
完整代码

来源:app/content/ruyipage_examples/06_screenshot.py · 仓库文件:06_screenshot.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 截图功能:整页、元素、base64、bytes
# 说明: 覆盖最常用的页面与元素截图输出能力。
# 你会学到:
#   1. 整页截图并保存到文件
#   2. 对单个元素或区域截图
#   3. 滚动到目标后截图
#   4. 获取截图的 base64 和 bytes 数据
# 相关 API: page.screenshot, ele.screenshot, page.scroll.to_see, page.scroll.to_bottom
# =============================

# -*- coding: utf-8 -*-
"""
示例6: 截图功能
测试功能:
- 整页截图
- 元素截图
- 保存为文件
- 获取base64
- 获取bytes
"""

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

def test_screenshot():
    """测试截图功能"""
    print("=" * 60)
    print("测试6: 截图功能")
    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)

        # 创建输出目录
        output_dir = os.path.join(os.path.dirname(__file__), 'output', 'screenshots')
        os.makedirs(output_dir, exist_ok=True)

        # 1. 整页截图
        print("\n1. 整页截图:")
        full_path = os.path.join(output_dir, 'full_page.png')
        page.screenshot(full_path)
        print(f"   ✓ 整页截图已保存: {full_path}")

        # 2. 元素截图
        print("\n2. 元素截图:")
        title = page.ele('#main-title')
        title_path = os.path.join(output_dir, 'title_element.png')
        title.screenshot(title_path)
        print(f"   ✓ 标题元素截图已保存: {title_path}")

        # 3. 表单区域截图
        print("\n3. 表单区域截图:")
        form_section = page.ele('#form-section')
        form_path = os.path.join(output_dir, 'form_section.png')
        form_section.screenshot(form_path)
        print(f"   ✓ 表单区域截图已保存: {form_path}")

        # 4. 按钮截图
        print("\n4. 按钮截图:")
        btn = page.ele('#click-btn')
        # 先滚动到按钮位置确保可见
        page.scroll.to_see(btn)
        page.wait(0.5)
        btn_path = os.path.join(output_dir, 'button.png')
        try:
            btn.screenshot(btn_path)
            print(f"   ✓ 按钮截图已保存: {btn_path}")
        except Exception as e:
            print(f"   ⚠ 按钮截图跳过(元素尺寸问题): {str(e)[:50]}")

        # 5. 获取截图的base64
        print("\n5. 获取截图base64:")
        base64_data = page.screenshot(as_base64=True)
        print(f"   ✓ Base64数据长度: {len(base64_data)} 字符")

        # 6. 获取截图的bytes
        print("\n6. 获取截图bytes:")
        bytes_data = page.screenshot(as_bytes=True)
        print(f"   ✓ Bytes数据长度: {len(bytes_data)} 字节")

        # 7. 元素base64截图
        print("\n7. 元素base64截图:")
        try:
            # 使用表单区域元素,它更稳定
            form_elem = page.ele('#form-section')
            elem_base64 = form_elem.screenshot(as_base64=True)
            print(f"   ✓ 元素Base64数据长度: {len(elem_base64)} 字符")
        except Exception as e:
            print(f"   ⚠ 元素base64截图跳过(元素尺寸问题): {str(e)[:50]}")

        # 8. 滚动后截图
        print("\n8. 滚动后截图:")
        page.scroll.to_bottom()
        page.wait(0.5)
        bottom_path = os.path.join(output_dir, 'page_bottom.png')
        page.screenshot(bottom_path)
        print(f"   ✓ 页面底部截图已保存: {bottom_path}")

        # 9. 表格截图
        print("\n9. 表格截图:")
        table = page.ele('#data-table')
        table_path = os.path.join(output_dir, 'table.png')
        try:
            page.scroll.to_see(table)
            page.wait(0.5)
            table.screenshot(table_path)
            print(f"   ✓ 表格截图已保存: {table_path}")
        except Exception as e:
            print(f"   ⚠ 表格截图跳过(元素尺寸问题): {str(e)[:50]}")

        print("\n" + "=" * 60)
        print("✓ 所有截图测试通过!")
        print(f"截图保存在: {output_dir}")
        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_screenshot()

JavaScript 执行与脚本能力

examples/07_javascript.py
GitHub 仓库 已同步源码 查看 GitHub 源码
debug intermediate
一句话定位

展示 run_js、参数传递、DOM 修改和 preload script 的调试价值。

涉及 API
page.run_jsele.run_jspage.get_realmspage.eval_handlepage.add_preload_scriptpage.remove_preload_script
展示的不同用法
  • 执行表达式并获取返回值
  • 向脚本传参
  • 直接修改 DOM 内容
  • 在元素上下文执行 JS
  • 读取 realms 并注入 preload script
适用场景

适合 javascript、script 和 preload-scripts 页面。

相关文档页面
推荐阅读
script-events-32script-input-29
完整代码

来源:app/content/ruyipage_examples/07_javascript.py · 仓库文件:07_javascript.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: JavaScript 执行与脚本能力
# 说明: 展示 run_js、参数传递、DOM 修改和 preload script 的调试价值。
# 你会学到:
#   1. 执行表达式并获取返回值
#   2. 向脚本传参
#   3. 直接修改 DOM 内容
#   4. 在元素上下文执行 JS
#   5. 读取 realms 并注入 preload script
# 相关 API: page.run_js, ele.run_js, page.get_realms, page.eval_handle, page.add_preload_script, page.remove_preload_script
# =============================

# -*- coding: utf-8 -*-
"""
示例7: JavaScript执行
测试功能:
- 执行JavaScript代码
- 获取返回值
- 传递参数
- 在元素上执行JS
- 修改页面内容
"""

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


def test_javascript():
    """测试JavaScript执行功能"""
    print("=" * 60)
    print("测试7: JavaScript执行")
    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. 执行简单的JavaScript
        print("\n1. 执行简单的JavaScript:")
        result = page.run_js("return 1 + 2")
        print(f"   1 + 2 = {result}")

        # 2. 获取页面信息
        print("\n2. 获取页面信息:")
        title = page.run_js("return document.title")
        print(f"   页面标题: {title}")

        url = page.run_js("return window.location.href")
        print(f"   页面URL: {url}")

        # 3. 修改页面内容
        print("\n3. 修改页面内容:")
        page.run_js(
            'document.getElementById("main-title").textContent = "标题已被JS修改"'
        )
        new_title = page.ele("#main-title").text
        print(f"   修改后的标题: {new_title}")

        # 4. 传递参数
        print("\n4. 传递参数到JavaScript:")
        result = page.run_js("return arguments[0] + arguments[1]", 10, 20)
        print(f"   10 + 20 = {result}")

        # 5. 在元素上执行JavaScript
        print("\n5. 在元素上执行JavaScript:")
        btn = page.ele("#click-btn")
        btn_text = btn.run_js("function() { return this.textContent; }")
        print(f"   按钮文本: {btn_text}")

        # 修改元素样式
        btn.run_js(
            'function() { this.style.background = "red"; this.style.color = "white"; }'
        )
        page.wait(0.5)
        print(f"   ✓ 按钮样式已修改")

        # 6. 获取元素属性
        print("\n6. 通过JS获取元素属性:")
        input_elem = page.ele("#text-input")
        placeholder = input_elem.run_js("function() { return this.placeholder; }")
        print(f"   输入框placeholder: {placeholder}")

        # 7. 执行复杂的JavaScript
        print("\n7. 执行复杂的JavaScript:")
        result = page.run_js("""
            (() => {
                const elements = document.querySelectorAll('.test-class');
                return Array.from(elements).map(el => el.textContent);
            })()
        """)
        print(f"   所有.test-class元素的文本: {result}")

        # 8. 修改输入框的值
        print("\n8. 通过JS修改输入框:")
        page.run_js("document.getElementById('text-input').value = 'JS设置的值'")
        value = page.ele("#text-input").value
        print(f"   输入框的值: {value}")

        # 9. 通过JS触发事件
        print("\n9. 通过JS触发事件:")
        page.run_js('document.getElementById("click-btn").click()')
        page.wait(0.5)
        result = page.ele("#click-result").text
        print(f"   点击结果: {result}")

        # 10. 获取计算样式
        print("\n10. 获取元素的计算样式:")
        color = page.run_js("""
            (() => {
                const elem = document.getElementById('click-btn');
                return window.getComputedStyle(elem).backgroundColor;
            })()
        """)
        print(f"   按钮背景色: {color}")

        # 11. 滚动到元素
        print("\n11. 通过JS滚动到元素:")
        page.run_js('document.getElementById("scroll-section").scrollIntoView()')
        page.wait(0.5)
        print(f"   ✓ 已滚动到滚动测试区域")

        # 12. 创建新元素
        print("\n12. 通过JS创建新元素:")
        page.run_js("""
            const div = document.createElement('div');
            div.id = 'js-created';
            div.textContent = 'JavaScript创建的元素';
            div.style.padding = '10px';
            div.style.background = '#ffeb3b';
            document.body.appendChild(div);
        """)
        page.wait(0.5)
        new_elem = page.ele("#js-created")
        print(f"   新元素文本: {new_elem.text}")

        # 13. 在 sandbox 中执行脚本
        print("\n13. 在sandbox中执行JavaScript:")
        sandbox_value = page.run_js(
            """
            globalThis.__ruyiSandboxCount = (globalThis.__ruyiSandboxCount || 0) + 1;
            return globalThis.__ruyiSandboxCount;
            """,
            as_expr=False,
            sandbox="example07",
        )
        normal_value = page.run_js("return globalThis.__ruyiSandboxCount || 0")
        print(f"   sandbox 计数: {sandbox_value}")
        print(f"   页面主世界计数: {normal_value}")

        # 14. 获取当前 context 的 realms
        print("\n14. 获取脚本Realms:")
        realms = page.get_realms()
        realm_types = sorted({realm.type or "unknown" for realm in realms})
        print(f"   Realm数量: {len(realms)}")
        print(f"   Realm类型: {realm_types}")

        # 15. 通过高层 script 结果对象执行并读取 handle/value
        print("\n15. 通过高层脚本接口执行:")
        eval_result = page.eval_handle(
            "JSON.stringify({title: document.title, ready: document.readyState})"
        )
        print(f"   evaluate 返回类型: {eval_result.type}")
        print(f"   evaluate 结果: {eval_result.result.value}")

        # 16. 预加载脚本
        print("\n16. 预加载脚本 add/removePreloadScript:")
        preload_id = page.add_preload_script("""() => {
            window.__example07Preload = 'preload-ready';
        }""")
        page.get(test_url)
        page.wait(1)
        preload_value = page.run_js("return window.__example07Preload")
        print(f"   preload脚本ID: {preload_id.id}")
        print(f"   preload注入结果: {preload_value}")
        page.remove_preload_script(preload_id)

        # 17. 移除预加载脚本后再次导航
        print("\n17. 移除预加载脚本后验证:")
        page.get(test_url)
        page.wait(1)
        removed_value = page.run_js("return window.__example07Preload || null")
        print(f"   移除后注入结果: {removed_value}")

        print("\n" + "=" * 60)
        print("✓ 所有JavaScript执行测试通过!")
        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_javascript()

Cookie 管理:读取、设置、过滤、删除

examples/08_cookies.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser intermediate
一句话定位

围绕登录态和会话管理的核心示例。

涉及 API
page.get_cookiespage.set_cookiespage.get_cookies_filteredpage.delete_cookiespage.browser.cookies
展示的不同用法
  • 读取服务端下发 Cookie
  • 通过 API 设置 Cookie
  • 按 name 等条件过滤 Cookie
  • 删除指定 Cookie 或清空全部 Cookie
  • 读取浏览器级 Cookie 视图
适用场景

适合 cookies 与 browser 页面。

相关文档页面
推荐阅读
quickstart-cloudfaretabs-09
完整代码

来源:app/content/ruyipage_examples/08_cookies.py · 仓库文件:08_cookies.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: Cookie 管理:读取、设置、过滤、删除
# 说明: 围绕登录态和会话管理的核心示例。
# 你会学到:
#   1. 读取服务端下发 Cookie
#   2. 通过 API 设置 Cookie
#   3. 按 name 等条件过滤 Cookie
#   4. 删除指定 Cookie 或清空全部 Cookie
#   5. 读取浏览器级 Cookie 视图
# 相关 API: page.get_cookies, page.set_cookies, page.get_cookies_filtered, page.delete_cookies, page.browser.cookies
# =============================

# -*- coding: utf-8 -*-
"""
示例8: Cookie管理
测试功能:
- 获取Cookie
- 设置Cookie
- 删除Cookie
- 清空所有Cookie
"""

import os
import sys
import io
import time

# 设置控制台输出编码为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__), ".."))
sys.path.insert(0, os.path.dirname(__file__))

from ruyipage import FirefoxPage, FirefoxOptions
from test_server import TestServer


def test_cookies():
    """测试Cookie管理功能"""
    print("=" * 60)
    print("测试8: Cookie管理")
    print("=" * 60)

    # 启动测试服务器
    server = TestServer(port=8888)
    server.start()

    opts = FirefoxOptions()
    opts.headless(False)
    page = FirefoxPage(opts)

    try:
        # 访问测试服务器的Cookie设置页面
        print("\n访问测试服务器...")
        page.get(server.get_url("/set-cookie"))
        page.wait(2)

        # 1. 获取服务器设置的Cookie
        print("\n1. 获取服务器设置的Cookie:")
        all_cookies = page.get_cookies(all_info=True)
        print(f"   共有 {len(all_cookies)} 个Cookie")
        for cookie in all_cookies:
            name = cookie.name
            value = cookie.value
            print(f"   - {name}: {value}")

        # 2. 通过API设置Cookie
        print("\n2. 通过API设置Cookie:")
        page.set_cookies(
            {
                "name": "api_cookie",
                "value": "api_value",
                "domain": "127.0.0.1",
                "path": "/",
            }
        )
        page.set_cookies(
            {"name": "user_id", "value": "12345", "domain": "127.0.0.1", "path": "/"}
        )
        print(f"   ✓ 已通过API设置2个Cookie")

        # 3. 验证Cookie已设置
        print("\n3. 验证Cookie已设置:")
        page.refresh()
        page.wait(1)
        all_cookies = page.get_cookies(all_info=True)
        print(f"   刷新后共有 {len(all_cookies)} 个Cookie")

        # 转换为字典方便查找
        cookies_dict = {}
        for c in all_cookies:
            cookies_dict[c.name] = c.value

        if "api_cookie" in cookies_dict:
            print(f"   ✓ api_cookie = {cookies_dict['api_cookie']}")
        if "user_id" in cookies_dict:
            print(f"   ✓ user_id = {cookies_dict['user_id']}")

        # 4. 设置带过期时间的Cookie
        print("\n4. 设置带过期时间的Cookie:")
        expire_time = int(time.time()) + 3600  # 1小时后过期
        page.set_cookies(
            {
                "name": "expire_cookie",
                "value": "will_expire",
                "domain": "127.0.0.1",
                "path": "/",
                "expiry": expire_time,
            }
        )
        print(f"   ✓ 已设置带过期时间的Cookie(1小时后过期)")

        # 5. 设置HttpOnly和Secure Cookie
        print("\n5. 设置HttpOnly和Secure Cookie:")
        page.set_cookies(
            {
                "name": "secure_cookie",
                "value": "secure_value",
                "domain": "127.0.0.1",
                "path": "/",
                "httpOnly": True,
                "secure": False,  # HTTP不能用secure=True
            }
        )
        print(f"   ✓ 已设置HttpOnly Cookie")

        # 6. 获取特定Cookie
        print("\n6. 获取特定Cookie:")
        all_cookies = page.get_cookies(all_info=True)
        cookies_dict = {}
        for c in all_cookies:
            cookies_dict[c.name] = c.value

        if "user_id" in cookies_dict:
            print(f"   user_id = {cookies_dict['user_id']}")
        if "expire_cookie" in cookies_dict:
            print(f"   expire_cookie = {cookies_dict['expire_cookie']}")

        # 6.1 简单 cookies 属性
        print("\n6.1 当前页面 cookies 属性:")
        simple_cookies = page.cookies
        print(f"   page.cookies 数量: {len(simple_cookies)}")

        # 6.2 setter 风格 Cookie API
        print("\n6.2 通过 page.set.cookies 设置Cookie:")
        page.set.cookies(
            {
                "name": "setter_cookie",
                "value": "setter_value",
                "domain": "127.0.0.1",
                "path": "/",
            }
        )
        page.wait(0.5)
        setter_cookies = {c.name: c.value for c in page.get_cookies()}
        print(f"   setter_cookie = {setter_cookies.get('setter_cookie')}")

        # 6.3 通过高层过滤 API 读取 Cookie
        print("\n6.3 通过高层过滤 API 读取Cookie:")
        filtered_cookies = page.get_cookies_filtered(name="user_id", all_info=True)
        print(f"   过滤结果数量: {len(filtered_cookies)}")
        if filtered_cookies:
            value = filtered_cookies[0].value
            print(f"   user_id(filtered) = {value}")

        # 7. 删除特定Cookie
        print("\n7. 删除特定Cookie:")
        page.delete_cookies(name="api_cookie")
        print(f"   ✓ 已删除 api_cookie")

        # 验证删除
        page.wait(0.5)
        all_cookies = page.get_cookies(all_info=True)
        cookies_dict = {}
        for c in all_cookies:
            cookies_dict[c.name] = c.value

        if "api_cookie" not in cookies_dict:
            print(f"   ✓ 确认 api_cookie 已被删除")
        else:
            print(f"   ⚠ api_cookie 仍然存在")

        # 7.1 通过公开删除接口删除 setter_cookie
        print("\n7.1 删除 setter_cookie:")
        page.delete_cookies(name="setter_cookie", domain="127.0.0.1")
        page.wait(0.5)
        cookies_after_remove = {c.name: c.value for c in page.get_cookies()}
        print(f"   setter_cookie 是否存在: {'setter_cookie' in cookies_after_remove}")

        # 8. 访问API验证Cookie发送
        print("\n8. 访问API验证Cookie发送:")
        page.get(server.get_url("/get-cookie"))
        page.wait(1)
        body_text = page.ele("tag:body").text
        print(f"   服务器收到的Cookie: {body_text[:100]}")

        # 9. 清空所有Cookie
        print("\n9. 清空所有Cookie:")
        page.delete_cookies()
        print(f"   ✓ 已清空所有Cookie")

        # 验证清空
        all_cookies = page.get_cookies()
        print(f"   清空后Cookie数量: {len(all_cookies)}")

        # 9.1 浏览器级 Cookie 读取
        print("\n9.1 浏览器级 cookies 读取:")
        browser_cookies = page.browser.cookies(all_info=False)
        print(f"   browser.cookies 数量: {len(browser_cookies)}")

        # 10. 重新设置并测试
        print("\n10. 重新设置Cookie并测试:")
        page.set_cookies(
            {
                "name": "final_cookie",
                "value": "final_value",
                "domain": "127.0.0.1",
                "path": "/",
            }
        )
        page.refresh()
        page.wait(1)

        all_cookies = page.get_cookies(all_info=True)
        cookies_dict = {}
        for c in all_cookies:
            cookies_dict[c.name] = c.value

        if "final_cookie" in cookies_dict:
            print(f"   ✓ final_cookie = {cookies_dict['final_cookie']}")
            print(f"   ✓ Cookie持久性测试通过")

        print("\n" + "=" * 60)
        print("✓ 所有Cookie管理测试通过!")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        page.wait(2)
        page.quit()
        server.stop()


if __name__ == "__main__":
    test_cookies()

标签页管理:新建、切换、查找、关闭、后台激活

examples/09_tabs.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser intermediate
一句话定位

展示 tab 作为日常页面操作的重要组织单位。

涉及 API
page.new_tabpage.tabs_countpage.tab_idspage.get_tabpage.get_tabspage.latest_tabpage.close_other_tabspage.browser.activate_tabpage.contexts.get_tree
展示的不同用法
  • 新建标签页并加载地址
  • 通过标题、索引等方式查找 tab
  • 获取所有标签页 ID
  • 激活后台标签页
  • 关闭单个标签或保留当前标签关闭其他标签
  • 观察 browsing context 树结构
适用场景

适合 tabs、browser 和 browsing-context 页面。

相关文档页面
推荐阅读
window-16page-01
完整代码

来源:app/content/ruyipage_examples/09_tabs.py · 仓库文件:09_tabs.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 标签页管理:新建、切换、查找、关闭、后台激活
# 说明: 展示 tab 作为日常页面操作的重要组织单位。
# 你会学到:
#   1. 新建标签页并加载地址
#   2. 通过标题、索引等方式查找 tab
#   3. 获取所有标签页 ID
#   4. 激活后台标签页
#   5. 关闭单个标签或保留当前标签关闭其他标签
# 相关 API: page.new_tab, page.tabs_count, page.tab_ids, page.get_tab, page.get_tabs, page.latest_tab, page.close_other_tabs, page.browser.activate_tab
# =============================

# -*- coding: utf-8 -*-
"""
示例9: 标签页管理
测试功能:
- 新建标签页
- 切换标签页
- 关闭标签页
- 获取标签页列表
- 标签页操作
"""

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


def test_tabs():
    """测试标签页管理功能"""
    print("=" * 60)
    print("测试9: 标签页管理")
    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. 获取当前标签页数量:")
        print(f"   标签页数量: {page.tabs_count}")

        tab2_url = "data:text/html,<html><head><title>Example Tab</title></head><body>tab2</body></html>"
        tab3_url = "data:text/html,<html><head><title>Wikipedia Tab</title></head><body>tab3</body></html>"

        # 2. 新建标签页
        print("\n2. 新建标签页:")
        tab2 = page.new_tab(tab2_url)
        page.wait(2)
        print(f"   ✓ 新标签页已创建")
        print(f"   新标签页标题: {tab2.title}")
        print(f"   当前标签页数量: {page.tabs_count}")

        # 3. 再新建一个标签页
        print("\n3. 再新建一个标签页:")
        tab3 = page.new_tab(tab3_url)
        page.wait(2)
        print(f"   ✓ 第三个标签页已创建")
        print(f"   标签页标题: {tab3.title}")
        print(f"   当前标签页数量: {page.tabs_count}")

        # 4. 获取所有标签页ID
        print("\n4. 获取所有标签页ID:")
        tab_ids = page.tab_ids
        print(f"   标签页ID列表: {tab_ids}")

        print("   当前页面 tab_id: {}".format(page.tab_id))

        # 5. 通过序号获取标签页
        print("\n5. 通过序号获取标签页:")
        first_tab = page.get_tab(1)
        print(f"   第1个标签页标题: {first_tab.title}")

        second_tab = page.get_tab(2)
        print(f"   第2个标签页标题: {second_tab.title}")

        # 6. 切换到第一个标签页
        print("\n6. 切换到第一个标签页:")
        first_tab.get(test_url)
        page.wait(1)
        print(f"   当前标题: {first_tab.title}")

        # 7. 获取最新的标签页
        print("\n7. 获取最新的标签页:")
        latest = page.latest_tab
        print(f"   最新标签页标题: {latest.title}")

        # 8. 通过标题查找标签页
        print("\n8. 通过标题查找标签页:")
        example_tab = page.get_tab(title="Example")
        if example_tab:
            print(f"   找到标签页: {example_tab.title}")
        else:
            print(f"   未找到包含'Example'的标签页")

        # 9. 通过URL查找标签页
        print("\n9. 通过URL查找标签页:")
        wiki_tab = page.get_tab(title="Wikipedia")
        if wiki_tab:
            print(f"   找到标签页: {wiki_tab.title}")
        else:
            print(f"   未找到包含'wikipedia'的标签页")

        # 10. 获取所有匹配的标签页
        print("\n10. 获取所有标签页:")
        all_tabs = page.get_tabs()
        print(f"   共有 {len(all_tabs)} 个标签页")
        for i, tab in enumerate(all_tabs, 1):
            print(f"   标签页{i}: {tab.title[:50]}")

        # 10.1 获取窗口句柄信息
        print("\n10.1 获取客户端窗口信息:")
        window_handles = page.browser.window_handles
        print(f"   clientWindows 数量: {len(window_handles)}")
        if window_handles:
            first_window = window_handles[0]
            print(f"   首个窗口状态: {first_window.get('state')}")

        # 10.2 读取 browsing context 树
        print("\n10.2 获取 browsing context 树:")
        tree = page.contexts.get_tree()
        contexts = tree.contexts
        print(f"   顶层 context 数量: {len(contexts)}")
        if contexts:
            print(f"   第一个 context: {contexts[0].context}")

        # 11. 关闭一个标签页
        print("\n11. 关闭第二个标签页:")
        second_tab.close()
        page.wait(1)
        print(f"   ✓ 标签页已关闭")
        print(f"   剩余标签页数量: {page.tabs_count}")

        # 12. 关闭其他标签页(保留第一个)
        print("\n12. 关闭其他标签页:")
        page.close_other_tabs(first_tab)
        page.wait(1)
        print(f"   ✓ 其他标签页已关闭")
        print(f"   剩余标签页数量: {page.tabs_count}")

        # 13. 新建后台标签页
        print("\n13. 新建后台标签页:")
        bg_tab = page.new_tab(tab2_url, background=True)
        page.wait(1)
        print(f"   ✓ 后台标签页已创建")
        print(f"   当前标签页数量: {page.tabs_count}")

        # 14. 激活标签页
        print("\n14. 激活后台标签页:")
        page.browser.activate_tab(bg_tab)
        page.wait(1)
        print(f"   ✓ 标签页已激活")

        # 15. 通过高层 context API 创建后台标签页
        print("\n15. 通过高层context API创建后台标签页:")
        bidi_tab_id = page.contexts.create_tab(
            background=True,
            reference_context=page.tab_id,
        )
        page.wait(1)
        print(f"   高层新标签页ID: {bidi_tab_id}")
        print(f"   当前标签页数量: {page.tabs_count}")

        # 清理纯 BiDi 新建的标签页,避免影响退出顺序
        bidi_tab = page.get_tab(bidi_tab_id)
        if bidi_tab:
            bidi_tab.close()
            page.wait(1)
            print("   ✓ 纯BiDi标签页已关闭")

        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_tabs()

滚动操作:页面滚动、容器滚动、滚动到元素

examples/10_scrolling.py
GitHub 仓库 已同步源码 查看 GitHub 源码
actions intermediate
一句话定位

围绕滚轮和滚动可见性组织的一组常见交互。

涉及 API
page.actions.scrollpage.scroll.to_bottompage.scroll.to_toppage.scroll.to_seeele.scroll.to_bottomele.scroll.to_topstates.is_in_viewport
展示的不同用法
  • 滚动到页面顶部与底部
  • 按像素距离滚动
  • 滚动直到目标元素进入视口
  • 控制容器内部滚动
  • 读取滚动位置用于调试
适用场景

适合 scrolling 页面,也可作为 actions 的子案例。

相关文档页面
推荐阅读
waits-04advanced-input-20
完整代码

来源:app/content/ruyipage_examples/10_scrolling.py · 仓库文件:10_scrolling.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 滚动操作:页面滚动、容器滚动、滚动到元素
# 说明: 围绕滚轮和滚动可见性组织的一组常见交互。
# 你会学到:
#   1. 滚动到页面顶部与底部
#   2. 按像素距离滚动
#   3. 滚动直到目标元素进入视口
#   4. 控制容器内部滚动
#   5. 读取滚动位置用于调试
# 相关 API: page.actions.scroll, page.scroll.to_bottom, page.scroll.to_top, page.scroll.to_see, ele.scroll.to_bottom, ele.scroll.to_top, states.is_in_viewport
# =============================

# -*- coding: utf-8 -*-
"""
示例10: 滚动操作
测试功能:
- 滚动到顶部/底部
- 滚动到元素
- 滚动指定距离
- 元素内滚动
"""

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


def test_scrolling():
    """测试滚动操作功能"""
    print("=" * 60)
    print("测试10: 滚动操作")
    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. 滚动到页面底部:")
        page.actions.scroll(0, 4000).perform()
        page.wait(1)
        print(f"   ✓ 已滚动到底部")

        # 2. 滚动到页面顶部
        print("\n2. 滚动到页面顶部:")
        page.actions.scroll(0, -4000).perform()
        page.wait(1)
        print(f"   ✓ 已滚动到顶部")

        # 3. 滚动到特定元素
        print("\n3. 滚动到特定元素:")
        table_section = page.ele("#table-section")
        while not table_section.states.is_in_viewport:
            page.actions.scroll(0, 500).perform()
            page.wait(0.2)
        page.wait(1)
        print(f"   ✓ 已滚动到表格区域")

        # 4. 滚动指定像素
        print("\n4. 向下滚动500像素:")
        page.actions.scroll(0, 500).perform()
        page.wait(1)
        print(f"   ✓ 已向下滚动")

        # 5. 向上滚动
        print("\n5. 向上滚动300像素:")
        page.actions.scroll(0, -300).perform()
        page.wait(1)
        print(f"   ✓ 已向上滚动")

        # 6. 滚动到元素使其可见
        print("\n6. 滚动到元素使其可见:")
        form_section = page.ele("#form-section")
        while not form_section.states.is_in_viewport:
            page.actions.scroll(0, -400).perform()
            page.wait(0.2)
        page.wait(1)
        print(f"   ✓ 元素已滚动到可见区域")

        # 7. 元素内部滚动
        print("\n7. 元素内部滚动:")
        scroll_container = page.ele("#scroll-container")

        # 滚动容器到可见区域
        while not scroll_container.states.is_in_viewport:
            page.actions.scroll(0, 400).perform()
            page.wait(0.2)
        page.wait(0.5)

        # 在容器内滚动
        scroll_container.scroll.to_bottom()
        page.wait(1)
        print(f"   ✓ 容器已滚动到底部")

        scroll_container.scroll.to_top()
        page.wait(1)
        print(f"   ✓ 容器已滚动到顶部")

        # 8. 滚动到容器内的元素
        print("\n8. 滚动到容器内的元素:")
        scroll_target = page.ele("#scroll-target")
        scroll_container.scroll.to_see(scroll_target)
        page.wait(1)
        print(f"   ✓ 已滚动到目标元素")

        # 9. 获取滚动位置
        print("\n9. 获取页面滚动位置:")
        scroll_pos = page.run_js("return {x: window.scrollX, y: window.scrollY}")
        print(
            f"   当前滚动位置: X={scroll_pos.get('x', 0)}, Y={scroll_pos.get('y', 0)}"
        )

        # 10. 平滑滚动
        print("\n10. 平滑滚动到顶部:")
        page.actions.scroll(0, -4000).perform()
        page.wait(2)
        print(f"   ✓ 平滑滚动完成")

        # 11. 滚动到页面特定位置
        print("\n11. 滚动到页面中间:")
        page.actions.scroll(0, 500).perform()
        page.wait(1)
        print(f"   ✓ 已滚动到指定位置")

        # 12. 将元素滚动到视图
        print("\n12. 将元素滚动到视图:")
        network_section = page.ele("#network-section")
        while not network_section.states.is_in_viewport:
            page.actions.scroll(0, 450).perform()
            page.wait(0.2)
        page.wait(1)
        print(f"   ✓ 元素已滚动到视图")

        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_scrolling()

网络拦截入门:mock、fail、改请求头、队列模式

examples/11_network_intercept.py
GitHub 仓库 已同步源码 查看 GitHub 源码
network intermediate
一句话定位

通过本地 TestServer 让请求拦截、mock 和 fail 行为可重复验证。

涉及 API
page.intercept.startpage.intercept.waitreq.mockreq.failreq.continue_requestpage.run_js
展示的不同用法
  • 拦截请求并直接 mock 响应
  • 主动 fail 请求
  • 修改请求头后继续请求
  • 无 handler 队列模式手动处理请求
适用场景

适合 network 与 intercept 页面。

相关文档页面
推荐阅读
advanced-network-18collector-28
完整代码

来源:app/content/ruyipage_examples/11_network_intercept.py · 仓库文件:11_network_intercept.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 网络拦截入门:mock、fail、改请求头、队列模式
# 说明: 通过本地 TestServer 让请求拦截、mock 和 fail 行为可重复验证。
# 你会学到:
#   1. 拦截请求并直接 mock 响应
#   2. 主动 fail 请求
#   3. 修改请求头后继续请求
#   4. 无 handler 队列模式手动处理请求
# 相关 API: page.intercept.start, page.intercept.wait, req.mock, req.fail, req.continue_request, page.run_js
# =============================

# -*- coding: utf-8 -*-
"""
示例11: 网络拦截(本地可重复版)

测试覆盖:
1) beforeRequestSent + mock 响应
2) beforeRequestSent + fail 请求
3) beforeRequestSent + 修改请求头并继续
4) 无 handler 队列模式 wait()

说明:
- 本示例全部使用本地 TestServer,避免外网波动导致结果不稳定。
- 所有拦截都在当前页面 context 内触发,便于复现与调试。
"""

import io
import os
import sys


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__), ".."))
sys.path.insert(0, os.path.dirname(__file__))

from ruyipage import FirefoxOptions, FirefoxPage
from test_server import TestServer


def test_network_intercept():
    """网络拦截演示主流程(适合初学者逐步阅读)。"""
    print("=" * 60)
    print("测试11: 网络拦截")
    print("=" * 60)

    server = TestServer(port=8888)
    server.start()

    opts = FirefoxOptions()
    opts.headless(False)
    page = FirefoxPage(opts)

    try:
        # 准备一个干净页面,确保后续 fetch 都在当前 context 内触发。
        page.get("about:blank")
        page.wait(0.5)

        # ==========================================================
        # 1) mock 响应
        # ==========================================================
        print("\n1. 拦截并 Mock 本地 API:")

        def mock_handler(req):
            if "/api/data" in req.url:
                print(f"   拦截到请求: {req.method} {req.url}")
                # 注意:mock 需要带上 CORS 头,不然浏览器会把跨域响应拦掉,
                # fetch 侧会看到 NetworkError。
                req.mock(
                    '{"status":"ok","data":{"message":"mocked-by-interceptor"}}',
                    status_code=200,
                    headers=[
                        {
                            "name": "content-type",
                            "value": {"type": "string", "value": "application/json"},
                        },
                        {
                            "name": "access-control-allow-origin",
                            "value": {"type": "string", "value": "*"},
                        },
                    ],
                )
                print("   ✓ 已返回 Mock 响应")
            else:
                # 不处理的请求必须显式继续,否则会一直挂起。
                req.continue_request()

        page.intercept.start(mock_handler, phases=["beforeRequestSent"])
        mocked = page.run_js(
            """
            return fetch(arguments[0]).then(r => r.json()).then(d => d.data.message).catch(e => String(e));
            """,
            server.get_url("/api/data"),
            as_expr=False,
        )
        print(f"   mock结果: {mocked}")
        page.intercept.stop()

        # ==========================================================
        # 2) fail 请求
        # ==========================================================
        print("\n2. 阻止本地 API 请求:")

        def fail_handler(req):
            if "/api/data" in req.url:
                print(f"   阻止请求: {req.url}")
                req.fail()
            else:
                req.continue_request()

        page.intercept.start(fail_handler, phases=["beforeRequestSent"])
        blocked = page.run_js(
            """
            return fetch(arguments[0]).then(() => 'unexpected-success').catch(e => 'blocked:' + e.name);
            """,
            server.get_url("/api/data"),
            as_expr=False,
        )
        print(f"   fail结果: {blocked}")
        page.intercept.stop()

        # ==========================================================
        # 3) 修改请求头并继续
        # ==========================================================
        print("\n3. 修改请求头并继续:")

        def header_handler(req):
            if "/api/headers" in req.url:
                # continue_request(headers=...) 会覆盖本次请求头。
                req.continue_request(
                    headers=[
                        {
                            "name": "X-Ruyi-Demo",
                            "value": {"type": "string", "value": "yes"},
                        },
                        {
                            "name": "User-Agent",
                            "value": {"type": "string", "value": "RuyiPage/Example11"},
                        },
                    ]
                )
                print("   ✓ 已注入自定义请求头")
            else:
                req.continue_request()

        page.intercept.start(header_handler, phases=["beforeRequestSent"])
        headers_json = page.run_js(
            """
            return fetch(arguments[0]).then(r => r.json()).catch(e => ({error:String(e)}));
            """,
            server.get_url("/api/headers"),
            as_expr=False,
        )
        x_header = ""
        if isinstance(headers_json, dict):
            x_header = headers_json.get("X-Ruyi-Demo") or headers_json.get(
                "x-ruyi-demo"
            )
        print(f"   服务器看到的 X-Ruyi-Demo: {x_header}")
        page.intercept.stop()

        # ==========================================================
        # 4) 无 handler 队列模式
        # ==========================================================
        print("\n4. 队列模式 wait() 捕获拦截请求:")
        # handler=None 表示不在回调里处理,而是进入队列,供 wait() 手动取出。
        page.intercept.start(handler=None, phases=["beforeRequestSent"])
        page.run_js(
            """
            fetch(arguments[0]).catch(() => null);
            return 'sent';
            """,
            server.get_url("/api/data"),
            as_expr=False,
        )
        req = page.intercept.wait(timeout=5)
        if req:
            print(f"   wait捕获: {req.method} {req.url}")
            # 队列模式下记得手动继续/中止请求,否则请求会悬挂。
            req.continue_request()
        else:
            print("   ⚠ 未在超时内捕获请求")
        page.intercept.stop()

        print("\n" + "=" * 60)
        print("✓ 网络拦截测试通过!")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        try:
            page.intercept.stop()
        except Exception:
            pass
        page.wait(1)
        page.quit()
        server.stop()


if __name__ == "__main__":
    test_network_intercept()

控制台监听:采集、过滤、等待与停止监听

examples/12_console_listener.py
GitHub 仓库 已同步源码 查看 GitHub 源码
debug intermediate
一句话定位

演示如何把 console 作为页面调试和自动化观测的一部分。

涉及 API
page.console.startpage.console.getpage.console.clearpage.console.waitpage.console.stoppage.run_js
展示的不同用法
  • 监听 console.log / warn / error
  • 获取全部日志记录
  • 按级别过滤关键日志
  • 等待特定日志出现
  • 停止监听避免噪音累积
适用场景

适合 console 和 events 章节。

相关文档页面
推荐阅读
log-input-33javascript-07
完整代码

来源:app/content/ruyipage_examples/12_console_listener.py · 仓库文件:12_console_listener.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 控制台监听:采集、过滤、等待与停止监听
# 说明: 演示如何把 console 作为页面调试和自动化观测的一部分。
# 你会学到:
#   1. 监听 console.log / warn / error
#   2. 获取全部日志记录
#   3. 按级别过滤关键日志
#   4. 等待特定日志出现
#   5. 停止监听避免噪音累积
# 相关 API: page.console.start, page.console.get, page.console.clear, page.console.wait, page.console.stop, page.run_js
# =============================

# -*- coding: utf-8 -*-
"""
示例12: 控制台监听
测试功能:
- 监听console.log
- 监听console.error
- 监听console.warn
- 获取日志内容
"""

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


def test_console_listener():
    """测试控制台监听功能"""
    print("=" * 60)
    print("测试12: 控制台监听")
    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. 启动控制台监听:")
        page.console.start()
        print(f"   ✓ 控制台监听已启动")

        # 2. 触发console.log
        print("\n2. 触发console.log:")
        log_btn = page.ele("text:console.log")
        log_btn.click_self()
        page.wait(1)

        logs = page.console.get()
        print(f"   捕获到 {len(logs)} 条日志")
        for log in logs:
            print(f"   - [{log.level}] {log.text}")

        # 3. 触发console.warn
        print("\n3. 触发console.warn:")
        warn_btn = page.ele("text:console.warn")
        warn_btn.click_self()
        page.wait(1)

        logs = page.console.get()
        print(f"   捕获到 {len(logs)} 条日志")
        for log in logs[-1:]:  # 只显示最新的
            print(f"   - [{log.level}] {log.text}")

        # 4. 触发console.error
        print("\n4. 触发console.error:")
        error_btn = page.ele("text:console.error")
        error_btn.click_self()
        page.wait(1)

        logs = page.console.get()
        print(f"   捕获到 {len(logs)} 条日志")
        for log in logs[-1:]:  # 只显示最新的
            print(f"   - [{log.level}] {log.text}")

        # 5. 触发console.info
        print("\n5. 触发console.info:")
        info_btn = page.ele("text:console.info")
        info_btn.click_self()
        page.wait(1)

        logs = page.console.get()
        print(f"   捕获到 {len(logs)} 条日志")

        # 5.1 级别过滤
        print("\n5.1 级别过滤 (error):")
        error_logs = page.console.get(level="error")
        print(f"   error日志数量: {len(error_logs)}")

        # 6. 清空日志
        print("\n6. 清空日志:")
        page.console.clear()
        logs = page.console.get()
        print(f"   清空后日志数量: {len(logs)}")

        # 7. 通过JS输出日志
        print("\n7. 通过JS输出日志:")
        page.run_js('console.log("通过JS输出的日志")')
        page.run_js('console.error("通过JS输出的错误")')
        page.wait(1)

        logs = page.console.get()
        print(f"   捕获到 {len(logs)} 条日志")
        for log in logs:
            print(f"   - [{log.level}] {log.text}")

        # 7.1 wait() 过滤等待
        print("\n7.1 wait() 等待指定日志:")
        page.run_js('console.error("wait-target-message")')
        waited = page.console.wait(level="error", text="wait-target-message", timeout=5)
        if waited:
            print(f"   ✓ wait捕获: [{waited.level}] {waited.text}")
        else:
            print("   ⚠ wait未捕获到目标日志")

        # 8. 停止监听
        print("\n8. 停止监听:")
        page.console.stop()
        print(f"   ✓ 控制台监听已停止")

        # 8.1 停止后不应继续积累新日志
        print("\n8.1 停止后验证不再捕获:")
        before = len(page.console.get())
        page.run_js('console.log("should-not-be-captured-after-stop")')
        page.wait(0.6)
        after = len(page.console.get())
        print(f"   停止前后日志数量: {before} -> {after}")

        print("\n" + "=" * 60)
        print("✓ 所有控制台监听测试通过!")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        try:
            page.console.stop()
        except:
            pass
        page.wait(2)
        page.quit()


if __name__ == "__main__":
    test_console_listener()

iframe 操作:多种获取方式、上下文访问与 with_frame

examples/13_iframe.py
GitHub 仓库 已同步源码 查看 GitHub 源码
structure intermediate
一句话定位

这是 iframe 专题的主示例,必须在文档中单列。

涉及 API
page.get_framespage.get_framepage.with_frameiframe.eleiframe.run_jsiframe.tab_idiframe.is_cross_origin
展示的不同用法
  • 通过 locator 获取 iframe
  • 通过 index 获取 iframe
  • 通过 context_id 获取 iframe
  • 在 iframe 内查找元素并点击
  • 在 iframe 内执行 JS
  • 判断 iframe 是否跨域
  • 使用 with_frame() 临时进入上下文后返回主页面
适用场景

适合 iframe 页面与复杂页面结构专题。

推荐阅读
shadow-14tabs-09
完整代码

来源:app/content/ruyipage_examples/13_iframe.py · 仓库文件:13_iframe.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: iframe 操作:多种获取方式、上下文访问与 with_frame
# 说明: 这是 iframe 专题的主示例,必须在文档中单列。
# 你会学到:
#   1. 通过 locator 获取 iframe
#   2. 通过 index 获取 iframe
#   3. 通过 context_id 获取 iframe
#   4. 在 iframe 内查找元素并点击
#   5. 在 iframe 内执行 JS
# 相关 API: page.get_frames, page.get_frame, page.with_frame, iframe.ele, iframe.run_js, iframe.tab_id, iframe.is_cross_origin
# =============================

# -*- coding: utf-8 -*-
"""示例13: iframe 操作(完整 + 复杂场景)

覆盖点:
1) 通过 locator/index/context_id 三种方式访问 iframe
2) 在 iframe 内元素查找、点击、JS 执行
3) 读取 iframe 列表和跨上下文稳定性
4) 使用 with_frame() 简化接口(新手友好)
"""

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


def test_iframe():
    """测试iframe操作功能"""
    print("=" * 60)
    print("测试13: iframe操作")
    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)

        # 滚动到iframe区域
        page.scroll.to_see(page.ele("#iframe-section"))
        page.wait(0.5)

        # 1. 获取iframe元素
        print("\n1. 获取iframe元素:")
        iframe_elem = page.ele("#test-iframe")
        print(f"   ✓ iframe元素已找到: {iframe_elem.tag}")

        # 1.1 获取所有 frame
        print("\n1.1 获取所有iframe:")
        frames = page.get_frames()
        print(f"   当前页面 frame 数量: {len(frames)}")

        # 2. 切换到iframe
        print("\n2. 切换到iframe:")
        iframe = page.get_frame("#test-iframe")
        print(f"   ✓ 已切换到iframe")

        # 2.1 通过 index 获取
        print("\n2.1 通过 index 获取iframe:")
        iframe_by_index = page.get_frame(index=0)
        print(f"   index=0 是否可用: {iframe_by_index is not None}")

        # 2.2 通过 context_id 获取
        print("\n2.2 通过 context_id 获取iframe:")
        iframe_by_ctx = page.get_frame(context_id=iframe.tab_id)
        print(f"   context_id 是否匹配: {iframe_by_ctx.tab_id == iframe.tab_id}")

        # 3. 在iframe中查找元素
        print("\n3. 在iframe中查找元素:")
        iframe_title = iframe.ele("tag:h1")
        if iframe_title:
            print(f"   iframe标题: {iframe_title.text}")
        else:
            print(f"   未找到iframe标题")

        # 3.1 iframe跨域判断
        print("\n3.1 iframe跨域判断:")
        print(f"   is_cross_origin: {iframe.is_cross_origin}")

        # 4. 在iframe中操作按钮
        print("\n4. 在iframe中操作按钮:")
        iframe_btn = iframe.ele("#iframe-btn")
        if iframe_btn:
            print(f"   找到iframe按钮: {iframe_btn.text}")
            iframe_btn.click_self()
            page.wait(1)
            print(f"   ✓ iframe按钮已点击")
        else:
            print(f"   未找到iframe按钮")

        # 5. 获取iframe内容
        print("\n5. 获取iframe内容:")
        iframe_html = iframe.html
        print(f"   iframe HTML长度: {len(iframe_html)} 字符")

        # 6. 在iframe中执行JS
        print("\n6. 在iframe中执行JS:")
        result = iframe.run_js("return document.body.innerHTML")
        print(f"   iframe body内容长度: {len(result)} 字符")

        # 6.1 iframe 内脚本修改 DOM
        print("\n6.1 iframe 内修改DOM并验证:")
        iframe.run_js(
            """
            const h = document.querySelector('h1');
            h.textContent = 'iframe内容-已修改';
            return h.textContent;
            """,
            as_expr=False,
        )
        changed_title = iframe.ele("tag:h1").text
        print(f"   修改后标题: {changed_title}")

        # 7. 切换回主页面
        print("\n7. 切换回主页面:")
        # 直接使用page对象即可,它始终指向主页面
        main_title = page.ele("#main-title")
        print(f"   主页面标题: {main_title.text}")

        # 7.1 验证主页面元素仍可操作
        click_btn = page.ele("#click-btn")
        click_btn.click_self()
        page.wait(0.4)
        click_result = page.ele("#click-result").text
        print(f"   主页面点击结果: {click_result}")

        # 8. 再次切换到iframe
        print("\n8. 再次切换到iframe:")
        iframe2 = page.get_frame("#test-iframe")
        iframe_title2 = iframe2.ele("tag:h1")
        if iframe_title2:
            print(f"   ✓ 再次访问iframe成功: {iframe_title2.text}")

        # 8.1 使用 with_frame 简化接口
        print("\n8.1 使用 with_frame() 访问iframe:")
        with page.with_frame("#test-iframe") as frame_ctx:
            frame_text = frame_ctx.ele("tag:h1").text
            print(f"   with_frame 读取标题: {frame_text}")

        # 8.2 with_frame 结束后仍在主页面上下文
        still_main = page.ele("#main-title").text
        print(f"   with_frame 退出后主页面标题: {still_main}")

        print("\n" + "=" * 60)
        print("✓ 所有iframe操作测试通过!")
        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_iframe()

Shadow DOM 与嵌套 iframe:open / closed shadow 全场景

examples/14_shadow_dom.py
GitHub 仓库 已同步源码 查看 GitHub 源码
structure advanced
一句话定位

这是 shadow DOM 与复杂边界专题的主示例,必须在文档中单列。

涉及 API
ele.with_shadowshadow_root.elepage.with_frameframe.ele
展示的不同用法
  • 进入主页面 open shadow root
  • 进入主页面 closed shadow root
  • 在 iframe 内访问 open shadow
  • 在 iframe 内访问 closed shadow
  • 组合使用 with_frame 和 with_shadow
  • 处理复杂边界后恢复主页面上下文
适用场景

适合 shadow-dom 和 complex-boundaries 专题页面。

推荐阅读
iframe-13elements-02
完整代码

来源:app/content/ruyipage_examples/14_shadow_dom.py · 仓库文件:14_shadow_dom.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: Shadow DOM 与嵌套 iframe:open / closed shadow 全场景
# 说明: 这是 shadow DOM 与复杂边界专题的主示例,必须在文档中单列。
# 你会学到:
#   1. 进入主页面 open shadow root
#   2. 进入主页面 closed shadow root
#   3. 在 iframe 内访问 open shadow
#   4. 在 iframe 内访问 closed shadow
#   5. 组合使用 with_frame 和 with_shadow
# 相关 API: ele.with_shadow, shadow_root.ele, page.with_frame, frame.ele
# =============================

# -*- coding: utf-8 -*-
"""示例14: Shadow DOM + 嵌套 iframe(open/closed 完整场景)

覆盖点:
1) 主页面 open shadow
2) 主页面 closed shadow(桥接函数)
3) 进入 iframe 后 open shadow
4) 进入 iframe 后 closed shadow(桥接函数)
5) with_frame / with_shadow 简洁 API
"""

import io
import os
import sys


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 FirefoxOptions, FirefoxPage


def test_shadow_dom():
    print("=" * 60)
    print("测试14: Shadow DOM + 嵌套iframe")
    print("=" * 60)

    opts = FirefoxOptions()
    opts.headless(False)
    page = FirefoxPage(opts)

    try:
        test_page = os.path.join(
            os.path.dirname(__file__), "test_pages", "complex_shadow_iframe.html"
        )
        test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
        page.get(test_url)
        page.wait(1)

        # 1) 主页面 open shadow
        print("\n1. 主页面 open shadow:")
        host_open = page.ele("#host-open-shadow")
        with host_open.with_shadow("open") as open_root:
            text = open_root.ele("#host-open-text").text
            print(f"   open文本: {text}")

        # 2) 主页面 closed shadow
        print("\n2. 主页面 closed shadow:")
        host_closed = page.ele("#host-closed-shadow")
        with host_closed.with_shadow("closed") as closed_root:
            text = closed_root.ele("#host-closed-text").text
            print(f"   closed文本: {text}")

        # 3) 进入 iframe,测试 open/closed shadow
        print("\n3. 进入iframe并测试 shadow:")
        with page.with_frame("#outer-iframe") as frame:
            inner_title = frame.ele("#inner-title").text
            print(f"   iframe标题: {inner_title}")

            inner_open = frame.ele("#inner-open-host")
            with inner_open.with_shadow("open") as inner_open_root:
                open_text = inner_open_root.ele("#inner-open-text").text
                print(f"   iframe open文本: {open_text}")

            inner_closed = frame.ele("#inner-closed-host")
            with inner_closed.with_shadow("closed") as inner_closed_root:
                closed_text = inner_closed_root.ele("#inner-closed-text").text
                print(f"   iframe closed文本: {closed_text}")

        # 4) 退出 with_frame 后仍可访问主页面
        print("\n4. 退出iframe后主页面可访问:")
        host_title = page.ele("#host-title").text
        print(f"   主页面标题: {host_title}")

        print("\n" + "=" * 60)
        print("✓ Shadow DOM + 嵌套iframe 测试通过!")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        page.wait(1)
        page.quit()


if __name__ == "__main__":
    test_shadow_dom()

综合实战:表单填写、数据提取、动态内容、截图与状态验证

examples/15_comprehensive.py
GitHub 仓库 已同步源码 查看 GitHub 源码
starter intermediate
一句话定位

最适合展示'一个真实业务脚本长什么样'的综合案例。

涉及 API
page.elepage.elesinputclick_selfselect.by_valuepage.wait.ele_displayedele.screenshotpage.screenshothoverpage.is_trusted
展示的不同用法
  • 完整填写表单
  • 提交并验证页面反馈
  • 抽取表格数据
  • 处理动态新增内容
  • 对关键区域截图
  • 验证状态变化与 isTrusted
适用场景

适合作为首页推荐案例和 examples 顶部重点展示。

相关文档页面
推荐阅读
elements-03screenshot-06
完整代码

来源:app/content/ruyipage_examples/15_comprehensive.py · 仓库文件:15_comprehensive.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 综合实战:表单填写、数据提取、动态内容、截图与状态验证
# 说明: 最适合展示'一个真实业务脚本长什么样'的综合案例。
# 你会学到:
#   1. 完整填写表单
#   2. 提交并验证页面反馈
#   3. 抽取表格数据
#   4. 处理动态新增内容
#   5. 对关键区域截图
# 相关 API: page.ele, page.eles, input, click_self, select.by_value, page.wait.ele_displayed, ele.screenshot, page.screenshot
# =============================

# -*- coding: utf-8 -*-
"""
示例15: 综合测试 - 实战场景
测试功能:
- 模拟真实的网页自动化场景
- 综合运用多种功能
- 表单填写和提交
- 数据提取
"""

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


def test_comprehensive():
    """综合测试 - 实战场景"""
    print("=" * 60)
    print("测试15: 综合测试 - 实战场景")
    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)

        print("\n场景1: 填写并提交表单")
        print("-" * 40)

        # 1. 填写文本输入框
        print("1. 填写表单字段...")
        page.ele("#text-input").input("张三")
        page.ele("#email-input").input("[email protected]")
        page.ele("#password-input").input("password123")
        page.ele("#number-input").input("25")
        page.ele("#textarea").input("这是一段测试文本\n包含多行内容")

        # 2. 选择复选框
        page.ele("#checkbox1").click_self()
        page.ele("#checkbox2").click_self()

        # 3. 选择单选框
        page.ele("#radio1").click_self()

        # 4. 选择下拉框
        select_ele = page.ele("#select-single")
        select_ok = select_ele.select.by_value("opt2", mode="native_only")
        if not select_ok:
            select_ok = select_ele.select.by_value("opt2", mode="compat")

        page.wait(0.5)
        print("   ✓ 表单填写完成")

        # 5. 提交表单
        print("2. 提交表单...")
        page.ele("#submit-btn").click_self()
        page.wait(1)

        # 6. 验证提交结果
        result = page.ele("#form-result").text
        print(f"   提交结果: {result[:100]}...")

        print("\n场景2: 数据提取")
        print("-" * 40)

        # 7. 提取表格数据
        print("1. 提取表格数据...")
        table_rows = page.eles("#data-table tbody tr")
        print(f"   表格共有 {len(table_rows)} 行数据")

        table_data = []
        for row in table_rows:
            cells = row.eles("tag:td")
            if len(cells) >= 4:
                row_data = {
                    "id": cells[0].text,
                    "name": cells[1].text,
                    "age": cells[2].text,
                    "city": cells[3].text,
                }
                table_data.append(row_data)
                print(f"   - {row_data}")

        print("\n场景3: 动态交互")
        print("-" * 40)

        # 8. 测试动态内容
        print("1. 触发延迟显示...")
        page.ele("text:显示延迟内容").click_self()
        dynamic_elem = page.wait.ele_displayed("#dynamic-content", timeout=5)
        if dynamic_elem:
            print(f"   ✓ 延迟内容已显示: {dynamic_elem.text}")

        # 9. 添加多个内容
        print("2. 添加多个动态内容...")
        for i in range(3):
            page.ele("text:添加内容").click_self()
            page.wait(0.3)

        new_items = page.eles("#content-container .result")
        print(f"   ✓ 已添加 {len(new_items)} 个新内容")

        print("\n场景4: 页面截图")
        print("-" * 40)

        # 10. 截取关键区域
        print("1. 截取关键区域...")
        output_dir = os.path.join(os.path.dirname(__file__), "output", "comprehensive")
        os.makedirs(output_dir, exist_ok=True)

        # 表单区域截图
        try:
            form_section = page.ele("#form-section")
            page.scroll.to_see(form_section)
            page.wait(0.5)
            form_section.screenshot(os.path.join(output_dir, "form_filled.png"))
            print(f"   ✓ 表单截图已保存")
        except Exception as e:
            print(f"   ⚠ 表单截图跳过: {str(e)[:50]}")

        # 表格截图
        try:
            table = page.ele("#data-table")
            page.scroll.to_see(table)
            page.wait(0.5)
            table.screenshot(os.path.join(output_dir, "table_data.png"))
            print(f"   ✓ 表格截图已保存")
        except Exception as e:
            print(f"   ⚠ 表格截图跳过: {str(e)[:50]}")

        # 整页截图
        page.screenshot(os.path.join(output_dir, "full_page.png"))
        print(f"   ✓ 整页截图已保存")

        print("\n场景5: 多次交互")
        print("-" * 40)

        # 11. 连续点击测试
        print("1. 连续点击按钮...")
        click_btn = page.ele("#click-btn")
        for i in range(5):
            click_btn.click_self()
            page.wait(0.2)

        result = page.ele("#click-result").text
        print(f"   点击结果: {result}")

        # 12. 鼠标悬停测试
        print("2. 鼠标悬停测试...")
        hover_target = page.ele("#hover-target")
        hover_target.hover()
        page.wait(0.5)
        hover_result = page.ele("#hover-result").text
        print(f"   悬停结果: {hover_result}")

        # 12.1 isTrusted 行为验证
        print("3. isTrusted 行为验证...")
        page.ele("#click-btn").click_self()
        page.wait(0.2)
        page.ele("#text-input").input("T", clear=False)
        page.wait(0.2)
        hover_target.hover()
        page.wait(0.2)
        print(f"   click isTrusted: {page.is_trusted('click')}")
        print(f"   keydown isTrusted: {page.is_trusted('keydown')}")
        print(f"   mouseenter isTrusted: {page.is_trusted('mouseenter')}")

        print("\n场景6: 数据验证")
        print("-" * 40)

        # 13. 验证元素状态
        print("1. 验证元素状态...")
        disabled_btn = page.ele("#disabled-btn")
        print(f"   禁用按钮是否可用: {disabled_btn.is_enabled}")
        print(f"   禁用按钮是否显示: {disabled_btn.is_displayed}")

        # 14. 验证输入值
        print("2. 验证输入值...")
        text_value = page.ele("#text-input").value
        email_value = page.ele("#email-input").value
        print(f"   文本输入框: {text_value}")
        print(f"   邮箱输入框: {email_value}")

        # 15. 验证选择状态
        print("3. 验证选择状态...")
        checkbox1_checked = page.ele("#checkbox1").is_checked
        radio1_checked = page.ele("#radio1").is_checked
        print(f"   复选框1选中: {checkbox1_checked}")
        print(f"   单选框1选中: {radio1_checked}")

        print("\n" + "=" * 60)
        print("✓ 综合测试全部通过!")
        print(f"截图保存在: {output_dir}")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        page.wait(3)
        page.quit()


if __name__ == "__main__":
    test_comprehensive()

窗口管理:尺寸、位置、状态流与标签切换

examples/16_window_management.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser intermediate
一句话定位

系统覆盖 window 与 viewport 的浏览器级操作。

涉及 API
page.rect.window_sizepage.rect.viewport_sizepage.rect.window_locationpage.rect.page_sizepage.window.maximizepage.window.minimizepage.window.fullscreenpage.window.set_sizepage.window.set_positionpage.window.centerpage.new_tab
展示的不同用法
  • 读取窗口和视口尺寸
  • 调整窗口大小与位置
  • 切换最大化、最小化、全屏等状态
  • 多标签创建和激活切换
适用场景

适合 window 与 browser 页面。

相关文档页面
推荐阅读
tabs-09browser-user-context-25
完整代码

来源:app/content/ruyipage_examples/16_window_management.py · 仓库文件:16_window_management.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 窗口管理:尺寸、位置、状态流与标签切换
# 说明: 系统覆盖 window 与 viewport 的浏览器级操作。
# 你会学到:
#   1. 读取窗口和视口尺寸
#   2. 调整窗口大小与位置
#   3. 切换最大化、最小化、全屏等状态
#   4. 多标签创建和激活切换
# 相关 API: page.rect.window_size, page.rect.viewport_size, page.rect.window_location, page.rect.page_size, page.window.maximize, page.window.minimize, page.window.fullscreen, page.window.set_size
# =============================

# -*- coding: utf-8 -*-
"""示例16: 浏览器窗口管理(完整场景)

测试覆盖:
1) 窗口/视口/页面尺寸与位置
2) 窗口状态流:normal -> maximize -> minimize -> fullscreen -> normal
3) 设置窗口大小、位置、居中
4) 多标签创建、激活、切换、关闭
"""

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


def test_window_management():
    """测试浏览器窗口管理功能"""
    print("=" * 60)
    print("测试16: 浏览器窗口管理")
    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. 获取当前窗口大小:")
        size = page.rect.window_size
        print(f"   窗口大小: {size[0]} x {size[1]}")

        # 2. 获取视口大小
        print("\n2. 获取视口大小:")
        viewport = page.rect.viewport_size
        print(f"   视口大小: {viewport[0]} x {viewport[1]}")

        # 3. 获取窗口位置
        print("\n3. 获取窗口位置:")
        location = page.rect.window_location
        print(f"   窗口位置: ({location[0]}, {location[1]})")

        # 4. 获取页面完整大小
        print("\n4. 获取页面完整大小:")
        page_size = page.rect.page_size
        print(f"   页面大小: {page_size[0]} x {page_size[1]}")

        # 5. 获取滚动位置
        print("\n5. 获取滚动位置:")
        scroll_pos = page.rect.scroll_position
        print(f"   滚动位置: ({scroll_pos[0]}, {scroll_pos[1]})")

        # 6. 滚动页面并检查位置
        print("\n6. 滚动页面并检查位置:")
        page.scroll.to_bottom()
        page.wait(0.5)
        new_scroll = page.rect.scroll_position
        print(f"   滚动后位置: ({new_scroll[0]}, {new_scroll[1]})")

        # 6.1 窗口状态流
        print("\n6.1 窗口状态流测试:")
        page.window.maximize()
        page.wait(0.5)
        print(f"   maximize 后窗口大小: {page.rect.window_size}")

        page.window.minimize()
        page.wait(0.5)
        print("   ✓ minimize 已调用")

        page.window.fullscreen()
        page.wait(0.5)
        print(f"   fullscreen 后视口大小: {page.rect.viewport_size}")

        page.window.normal()
        page.wait(0.5)
        print("   ✓ normal 已恢复")

        # 6.2 设置窗口尺寸与位置并居中
        print("\n6.2 设置窗口尺寸/位置/居中:")
        page.window.set_size(1200, 820)
        page.window.set_position(40, 40)
        page.wait(0.5)
        print(f"   调整后窗口大小: {page.rect.window_size}")
        print(f"   调整后窗口位置: {page.rect.window_location}")

        page.window.center()
        page.wait(0.5)
        print(f"   居中后窗口位置: {page.rect.window_location}")

        # 7. 创建新标签页
        print("\n7. 创建新标签页:")
        new_tab = page.new_tab()
        page.wait(1)
        print(f"   ✓ 新标签页已创建")

        # 8. 在新标签页中打开页面
        print("\n8. 在新标签页中打开页面:")
        new_tab.get("https://www.example.com")
        new_tab.wait(2)
        print(f"   新标签页URL: {new_tab.url}")
        print(f"   新标签页标题: {new_tab.title}")

        # 9. 切换回原标签页
        print("\n9. 切换回原标签页:")
        # 使用 browser.activate_tab 显式切回原 tab
        page.browser.activate_tab(page.tab_id)
        page.wait(0.5)
        page.get(test_url)
        page.wait(0.5)
        print(f"   当前标签页标题: {page.title}")

        # 9.1 新建后台标签页并激活
        print("\n9.1 后台标签页激活测试:")
        bg_tab = page.new_tab("https://www.example.com", background=True)
        page.wait(1)
        print(f"   后台标签页ID: {bg_tab.tab_id}")
        page.browser.activate_tab(bg_tab)
        page.wait(1)
        print(f"   激活后标题: {bg_tab.title}")

        # 切回主 tab,避免后续关闭干扰
        page.browser.activate_tab(page.tab_id)
        page.wait(0.5)

        # 10. 关闭新标签页
        print("\n10. 关闭新标签页:")
        new_tab.close()
        try:
            bg_tab.close()
        except Exception:
            pass
        page.wait(0.5)
        print(f"   ✓ 新标签页已关闭")

        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_window_management()

用户提示框处理:自动策略与步骤式登录 API

examples/17_user_prompts.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser intermediate
一句话定位

专门讲 alert、confirm、prompt 及其自动策略。

涉及 API
opts.set_user_prompt_handlerpage.set_prompt_handlerpage.clear_prompt_handlerpage.get_last_prompt_openedpage.get_last_prompt_closedpage.prompt_login
展示的不同用法
  • 设置全局 alert / confirm / prompt 处理策略
  • 给 prompt 设置默认输入文本
  • 获取最近一次 prompt 打开和关闭信息
  • 使用步骤式 API 完成 prompt 登录
适用场景

适合 prompts 页面。

相关文档页面
推荐阅读
browsing-events-30
完整代码

来源:app/content/ruyipage_examples/17_user_prompts.py · 仓库文件:17_user_prompts.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 用户提示框处理:自动策略与步骤式登录 API
# 说明: 专门讲 alert、confirm、prompt 及其自动策略。
# 你会学到:
#   1. 设置全局 alert / confirm / prompt 处理策略
#   2. 给 prompt 设置默认输入文本
#   3. 获取最近一次 prompt 打开和关闭信息
#   4. 使用步骤式 API 完成 prompt 登录
# 相关 API: opts.set_user_prompt_handler, page.set_prompt_handler, page.clear_prompt_handler, page.get_last_prompt_opened, page.get_last_prompt_closed, page.prompt_login
# =============================

# -*- coding: utf-8 -*-
"""示例17: 用户提示框处理(登录式步骤 API)

演示:
1) 自动策略 API
2) 步骤式 prompt 登录 API
3) 触发模式切换:mouse / keyboard
"""

import io
import os
import sys


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 FirefoxOptions, FirefoxPage


def test_user_prompts():
    print("=" * 60)
    print("测试17: 用户提示框处理")
    print("=" * 60)

    opts = FirefoxOptions()
    opts.headless(False)
    opts.set_user_prompt_handler(
        {
            "alert": "accept",
            "confirm": "accept",
            "prompt": "ignore",
            "default": "accept",
        }
    )
    page = FirefoxPage(opts)

    try:
        test_page = os.path.join(
            os.path.dirname(__file__), "test_pages", "native_user_prompts_test.html"
        )
        test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
        page.get(test_url)
        page.wait(0.8)

        page.set_prompt_handler(
            alert="accept",
            confirm="accept",
            prompt="ignore",
            default="accept",
            prompt_text="张三",
        )

        print("\n1. alert(自动策略):")
        page.ele("#alert-btn").click_self()
        page.wait(0.3)
        print(f"   result: {page.ele('#alert-result').text}")

        print("\n2. confirm(自动策略):")
        page.ele("#confirm-btn").click_self()
        page.wait(0.3)
        print(f"   result: {page.ele('#confirm-result').text}")
        print(f"   opened: {page.get_last_prompt_opened()}")
        print(f"   closed: {page.get_last_prompt_closed()}")

        print("\n3. prompt 登录(步骤式 API / mouse):")
        page.clear_prompt_handler()
        page.get(test_url)
        page.wait(0.8)
        page.prompt_login(
            "#login-prompt-btn", "alice", "s3cr3t", trigger="mouse", timeout=2
        )
        page.wait(0.5)
        print(f"   result: {page.ele('#prompt-result').text}")

        print("\n4. prompt 登录(步骤式 API / keyboard):")
        page.get(test_url)
        page.wait(0.8)
        page.prompt_login(
            "#login-prompt-btn", "bob", "654321", trigger="keyboard", timeout=2
        )
        page.wait(0.5)
        print(f"   result: {page.ele('#prompt-result').text}")

        print("\n" + "=" * 60)
        print("✓ 用户提示框自动策略 + 登录式步骤 API 测试完成!")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        try:
            page.clear_prompt_handler()
        except Exception:
            pass
        page.wait(1)
        page.quit()


if __name__ == "__main__":
    test_user_prompts()

高级网络功能:请求/响应拦截、mock、阻止、fetchError

examples/18_advanced_network.py
GitHub 仓库 已同步源码 查看 GitHub 源码
network advanced
一句话定位

展示 intercept 与 events 组合后的完整网络控制能力。

涉及 API
page.intercept.start_requestspage.intercept.start_responsesreq.continue_requestreq.mockreq.failreq.continue_responsepage.events.startpage.events.wait
展示的不同用法
  • 在 beforeRequestSent 阶段继续请求
  • 修改请求头
  • mock JSON 响应
  • 阻止请求
  • 在 responseStarted 阶段修改响应状态
  • 监听 fetchError
适用场景

适合 network、intercept 和 anti-bot 章节。

相关文档页面
推荐阅读
network-intercept-11collector-28network-events-31
完整代码

来源:app/content/ruyipage_examples/18_advanced_network.py · 仓库文件:18_advanced_network.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 高级网络功能:请求/响应拦截、mock、阻止、fetchError
# 说明: 展示 intercept 与 events 组合后的完整网络控制能力。
# 你会学到:
#   1. 在 beforeRequestSent 阶段继续请求
#   2. 修改请求头
#   3. mock JSON 响应
#   4. 阻止请求
#   5. 在 responseStarted 阶段修改响应状态
# 相关 API: page.intercept.start_requests, page.intercept.start_responses, req.continue_request, req.mock, req.fail, req.continue_response, page.events.start, page.events.wait
# =============================

# -*- coding: utf-8 -*-
"""示例18: 高级网络功能(完整场景)

覆盖:
1) beforeRequestSent 拦截 + 继续请求
2) 修改请求头
3) mock 响应
4) 阻止请求
5) responseStarted 阶段修改响应状态
6) 队列模式 wait() 手动处理
7) fetchError 事件监听
"""

import io
import os
import sys


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__), ".."))
sys.path.insert(0, os.path.dirname(__file__))

from ruyipage import FirefoxOptions, FirefoxPage
from ruyipage._functions.tools import find_free_port
from test_server import TestServer


def test_advanced_network():
    print("=" * 60)
    print("测试18: 高级网络功能")
    print("=" * 60)

    server = TestServer(port=find_free_port(8889, 8989))
    server.start()

    opts = FirefoxOptions()
    opts.headless(False)
    page = FirefoxPage(opts)

    fetch_error_list = []

    try:
        page.get("about:blank")
        page.wait(0.6)

        # 1) 请求拦截
        print("\n1. beforeRequestSent 拦截 + 继续请求:")
        intercepted = []

        def req_handler(req):
            if "/api/data" in req.url:
                intercepted.append(req.url)
                print(f"   拦截到请求: {req.method} {req.url}")
            req.continue_request()

        page.intercept.start_requests(req_handler)
        data = page.run_js(
            """
            return fetch(arguments[0]).then(r => r.json()).then(d => d.status).catch(e => String(e));
            """,
            server.get_url("/api/data"),
            as_expr=False,
        )
        page.wait(0.5)
        print(f"   请求结果: {data}")
        print(f"   拦截计数: {len(intercepted)}")
        page.intercept.stop()

        # 2) 修改请求头
        print("\n2. 修改请求头:")

        def header_handler(req):
            if "/api/headers" in req.url:
                req.continue_request(
                    headers=[
                        {
                            "name": "X-Ruyi-Demo",
                            "value": {"type": "string", "value": "yes"},
                        },
                        {
                            "name": "User-Agent",
                            "value": {"type": "string", "value": "RuyiPage/Example18"},
                        },
                    ]
                )
                print("   ✓ 已注入请求头")
            else:
                req.continue_request()

        page.intercept.start_requests(header_handler)
        headers = page.run_js(
            """
            return fetch(arguments[0]).then(r => r.json()).catch(e => ({error:String(e)}));
            """,
            server.get_url("/api/headers"),
            as_expr=False,
        )
        print(
            f"   X-Ruyi-Demo: {headers.get('X-Ruyi-Demo') or headers.get('x-ruyi-demo')}"
        )
        page.intercept.stop()

        # 3) mock 响应
        print("\n3. mock 响应:")

        def mock_handler(req):
            if "/api/data" in req.url:
                req.mock(
                    '{"status":"mocked","data":{"message":"这是Mock数据"}}',
                    status_code=200,
                    headers=[
                        {
                            "name": "content-type",
                            "value": {"type": "string", "value": "application/json"},
                        },
                        {
                            "name": "access-control-allow-origin",
                            "value": {"type": "string", "value": "*"},
                        },
                    ],
                )
                print("   ✓ 已返回Mock响应")
            else:
                req.continue_request()

        page.intercept.start_requests(mock_handler)
        mock_msg = page.run_js(
            """
            return fetch(arguments[0]).then(r => r.json()).then(d => d.status + ':' + d.data.message).catch(e => String(e));
            """,
            server.get_url("/api/data"),
            as_expr=False,
        )
        print(f"   Mock结果: {mock_msg}")
        page.intercept.stop()

        # 4) 阻止请求
        print("\n4. 阻止请求:")

        def block_handler(req):
            if "/api/error" in req.url:
                req.fail()
                print("   ✓ 请求已阻止")
            else:
                req.continue_request()

        page.intercept.start_requests(block_handler)
        blocked = page.run_js(
            """
            return fetch(arguments[0]).then(() => 'unexpected-success').catch(e => 'blocked:' + e.name);
            """,
            server.get_url("/api/error"),
            as_expr=False,
        )
        print(f"   阻止结果: {blocked}")
        page.intercept.stop()

        # 5) 响应阶段修改状态码
        print("\n5. responseStarted 阶段修改响应状态码:")

        def response_handler(req):
            if "/api/data" in req.url:
                req.continue_response(status_code=299, reason_phrase="RuyiModified")
                print("   ✓ 响应状态码已改为 299")
            else:
                req.continue_response()

        page.intercept.start_responses(response_handler)
        modified_status = page.run_js(
            """
            return fetch(arguments[0]).then(r => r.status + ':' + r.statusText).catch(e => String(e));
            """,
            server.get_url("/api/data"),
            as_expr=False,
        )
        print(f"   响应状态: {modified_status}")
        page.intercept.stop()

        # 6) 队列模式
        print("\n6. 队列模式 wait() 手动处理:")
        page.intercept.start(handler=None, phases=["beforeRequestSent"])
        page.run_js(
            """
            fetch(arguments[0]).catch(() => null);
            return 'sent';
            """,
            server.get_url("/api/data"),
            as_expr=False,
        )
        queued = page.intercept.wait(timeout=5)
        if queued:
            print(f"   wait捕获: {queued.method} {queued.url}")
            queued.continue_request()
        else:
            print("   ⚠ 未捕获到请求")
        page.intercept.stop()

        # 7) fetchError 事件监听
        print("\n7. fetchError 事件监听:")
        page.events.start(["network.fetchError"], contexts=[page.tab_id])
        page.events.clear()

        page.intercept.start_requests(block_handler)
        page.run_js(
            """
            fetch(arguments[0]).catch(() => null);
            return 'sent';
            """,
            server.get_url("/api/error"),
            as_expr=False,
        )
        page.wait(1)
        page.intercept.stop()
        while True:
            evt = page.events.wait("network.fetchError", timeout=0.2)
            if not evt:
                break
            fetch_error_list.append(evt)
        print(f"   fetchError 事件数量: {len(fetch_error_list)}")

        print("\n" + "=" * 60)
        print("✓ 所有高级网络功能测试通过!")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        try:
            page.intercept.stop()
        except Exception:
            pass
        try:
            page.events.stop()
        except Exception:
            pass
        page.wait(1)
        page.quit()
        server.stop()


if __name__ == "__main__":
    test_advanced_network()

PDF 打印:纸张尺寸、方向、边距、页范围与导出数据

examples/19_pdf_printing.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser intermediate
一句话定位

集中说明 save_pdf 的参数覆盖能力。

涉及 API
page.save_pdfpage.pdf
展示的不同用法
  • 基础 PDF 导出
  • A4 纸张和背景打印
  • 横向打印与缩放
  • 自定义页边距和页范围
  • 获取 PDF bytes 与 base64
适用场景

适合 pdf 页面。

相关文档页面
推荐阅读
screenshot-06basic-navigation-01
完整代码

来源:app/content/ruyipage_examples/19_pdf_printing.py · 仓库文件:19_pdf_printing.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: PDF 打印:纸张尺寸、方向、边距、页范围与导出数据
# 说明: 集中说明 save_pdf 的参数覆盖能力。
# 你会学到:
#   1. 基础 PDF 导出
#   2. A4 纸张和背景打印
#   3. 横向打印与缩放
#   4. 自定义页边距和页范围
#   5. 获取 PDF bytes 与 base64
# 相关 API: page.save_pdf, page.pdf
# =============================

# -*- coding: utf-8 -*-
"""示例19: PDF 打印(完整参数覆盖)"""

import io
import os
import sys


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 FirefoxOptions, FirefoxPage


def _size_of(path):
    return os.path.getsize(path) if os.path.exists(path) else 0


def test_pdf_printing():
    print("=" * 60)
    print("测试19: PDF打印功能")
    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)

        output_dir = os.path.join(os.path.dirname(__file__), "output", "pdf")
        os.makedirs(output_dir, exist_ok=True)

        # 1) 基本打印
        print("\n1. 基本PDF打印:")
        p1 = os.path.join(output_dir, "basic.pdf")
        page.save_pdf(p1)
        print(f"   ✓ 已保存: {p1} ({_size_of(p1)} bytes)")

        # 2) A4 + 背景
        print("\n2. A4 + 背景:")
        p2 = os.path.join(output_dir, "a4_bg.pdf")
        page.save_pdf(
            p2,
            page={"width": 21.0, "height": 29.7},
            background=True,
        )
        print(f"   ✓ 已保存: {p2} ({_size_of(p2)} bytes)")

        # 3) 横向 + 缩放 + 页边距
        print("\n3. 横向 + 缩放 + 页边距:")
        p3 = os.path.join(output_dir, "landscape_scaled.pdf")
        page.save_pdf(
            p3,
            orientation="landscape",
            scale=0.9,
            margin={
                "top": 1.2,
                "bottom": 1.2,
                "left": 1.0,
                "right": 1.0,
            },
        )
        print(f"   ✓ 已保存: {p3} ({_size_of(p3)} bytes)")

        # 4) 指定页范围 + shrinkToFit
        print("\n4. 指定页范围 + shrinkToFit:")
        p4 = os.path.join(output_dir, "page_ranges_shrink.pdf")
        page.save_pdf(
            p4,
            page_ranges=["1-2"],
            shrink_to_fit=True,
        )
        print(f"   ✓ 已保存: {p4} ({_size_of(p4)} bytes)")

        # 5) 获取 bytes/base64
        print("\n5. 获取 bytes / base64:")
        pdf_bytes = page.pdf(page_ranges=["1"])
        pdf_b64 = (
            __import__("base64")
            .b64encode(page.pdf(shrink_to_fit=False))
            .decode("ascii")
        )
        print(f"   bytes长度: {len(pdf_bytes)}")
        print(f"   base64长度: {len(pdf_b64)}")

        if not (len(pdf_bytes) > 100 and len(pdf_b64) > 100):
            raise RuntimeError("PDF 输出长度异常")

        print("\n" + "=" * 60)
        print("✓ 所有PDF打印测试通过!")
        print(f"PDF保存在: {output_dir}")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        page.wait(1)
        page.quit()


if __name__ == "__main__":
    test_pdf_printing()

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

examples/20_advanced_input.py
GitHub 仓库 已同步源码 查看 GitHub 源码
actions advanced
一句话定位

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

涉及 API
page.actions.combopage.actions.presspage.actions.double_clickpage.actions.right_clickpage.actions.middle_clickpage.actions.dragpage.actions.drag_topage.actions.scrollpage.actions.human_movepage.actions.human_clickpage.actions.human_typepage.touch.tappage.touch.long_press
展示的不同用法
  • 组合键与复制粘贴
  • 双击、右击、中键点击
  • 拖拽和元素级 drag_to
  • 滚轮和 hover
  • 拟人化移动、点击和输入
  • 触摸点击与长按
  • 文件上传和键盘导航
适用场景

适合 actions、touch、drag-drop、forms 和 is-trusted 页面。

推荐阅读
actions-chain-05native-drag-35native-select-36
完整代码

来源: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)

Emulation 模块:UA、地理位置、时区、语言、屏幕与移动预设

examples/21_emulation.py
GitHub 仓库 已同步源码 查看 GitHub 源码
emulation advanced
一句话定位

讲清 emulation 在 Firefox 路线上的主要能力边界。

涉及 API
page.emulation.set_user_agentpage.emulation.set_geolocationpage.emulation.set_timezonepage.emulation.set_localepage.emulation.set_screen_orientationpage.emulation.set_screen_sizepage.emulation.set_network_offlinepage.emulation.set_touch_enabledpage.emulation.set_javascript_enabledpage.emulation.apply_mobile_preset
展示的不同用法
  • 覆盖 UA
  • 模拟地理位置、时区和语言
  • 设置屏幕尺寸和方向
  • 切换网络离线状态
  • 尝试开启触摸能力
  • 应用移动端预设
适用场景

适合 emulation、mobile 和 anti-bot 页面。

相关文档页面
推荐阅读
mobile-google-21memulation-27
完整代码

来源:app/content/ruyipage_examples/21_emulation.py · 仓库文件:21_emulation.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: Emulation 模块:UA、地理位置、时区、语言、屏幕与移动预设
# 说明: 讲清 emulation 在 Firefox 路线上的主要能力边界。
# 你会学到:
#   1. 覆盖 UA
#   2. 模拟地理位置、时区和语言
#   3. 设置屏幕尺寸和方向
#   4. 切换网络离线状态
#   5. 尝试开启触摸能力
# 相关 API: page.emulation.set_user_agent, page.emulation.set_geolocation, page.emulation.set_timezone, page.emulation.set_locale, page.emulation.set_screen_orientation, page.emulation.set_screen_size, page.emulation.set_network_offline, page.emulation.set_touch_enabled
# =============================

# -*- coding: utf-8 -*-
"""示例21: Emulation 模块测试(严格结果版)"""

import io
import sys

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 FirefoxPage


class Row:
    def __init__(self):
        self.rows = []

    def add(self, name, status, detail=""):
        self.rows.append((name, status, detail))
        print(f"  {status}: {name}")
        if detail:
            print(f"     详情: {detail}")

    def table(self):
        print("\n" + "=" * 70)
        print("测试结果汇总")
        print("=" * 70)
        print(f"{'序号':<5} {'项目':<32} {'状态':<8} {'说明'}")
        print("-" * 70)
        for i, (name, status, detail) in enumerate(self.rows, 1):
            print(f"{i:<5} {name:<32} {status:<8} {detail[:32]}")
        print("-" * 70)


def test_emulation():
    print("=" * 70)
    print("测试 21: Emulation 模块")
    print("=" * 70)

    page = FirefoxPage()
    rows = Row()

    try:
        page.get("data:text/html,<html><body><h1>Emulation Test</h1></body></html>")

        # 1) UA
        original_ua = page.run_js("navigator.userAgent")
        custom_ua = (
            "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) "
            "AppleWebKit/605.1.15"
        )
        page.emulation.set_user_agent(custom_ua)
        page.refresh()
        new_ua = page.run_js("navigator.userAgent")
        rows.add(
            "User-Agent 覆盖",
            "✓ 通过" if custom_ua in str(new_ua) else "✗ 失败",
            f"UA={'命中' if custom_ua in str(new_ua) else '未命中'}",
        )

        # 2) geolocation
        try:
            page.emulation.set_geolocation(39.9042, 116.4074, accuracy=100)
            rows.add("地理位置覆盖", "✓ 通过", "命令执行成功")
        except Exception as e:
            rows.add("地理位置覆盖", "✗ 失败", str(e))

        # 3) timezone
        try:
            page.emulation.set_timezone("America/New_York")
            page.refresh()
            timezone = page.run_js("Intl.DateTimeFormat().resolvedOptions().timeZone")
            ok = "New_York" in str(timezone)
            rows.add("时区覆盖", "✓ 通过" if ok else "✗ 失败", f"当前={timezone}")
        except Exception as e:
            rows.add("时区覆盖", "✗ 失败", str(e))

        # 4) locale
        try:
            page.emulation.set_locale("ja-JP")
            page.refresh()
            language = page.run_js("navigator.language")
            ok = "ja" in str(language).lower()
            rows.add("语言覆盖", "✓ 通过" if ok else "✗ 失败", f"当前={language}")
        except Exception as e:
            rows.add("语言覆盖", "✗ 失败", str(e))

        # 5) orientation
        try:
            page.emulation.set_screen_orientation("landscape-primary", angle=90)
            page.refresh()
            orientation = page.run_js("screen.orientation.type")
            ok = "landscape" in str(orientation)
            rows.add(
                "屏幕方向覆盖", "✓ 通过" if ok else "✗ 失败", f"当前={orientation}"
            )
        except Exception as e:
            rows.add("屏幕方向覆盖", "✗ 失败", str(e))

        # 6) screen settings
        try:
            page.emulation.set_screen_size(1920, 1080, device_pixel_ratio=2.0)
            page.refresh()
            sw = page.run_js("screen.width")
            sh = page.run_js("screen.height")
            ok = sw == 1920 and sh == 1080
            rows.add("屏幕设置覆盖", "✓ 通过" if ok else "✗ 失败", f"当前={sw}x{sh}")
        except Exception as e:
            rows.add("屏幕设置覆盖", "✗ 失败", str(e))

        # 7) network offline
        supported = page.emulation.set_network_offline(True)
        rows.add(
            "网络条件模拟",
            "✓ 通过" if supported else "⚠ 不支持",
            "离线模式" if supported else "当前 Firefox 未实现",
        )

        # 8) touch override
        supported = page.emulation.set_touch_enabled(True)
        rows.add(
            "触摸模拟",
            "✓ 通过" if supported else "⚠ 不支持",
            "启用触摸" if supported else "当前 Firefox 未实现",
        )

        # 9) javascript enable
        supported = page.emulation.set_javascript_enabled(True)
        rows.add(
            "JavaScript 开关",
            "✓ 通过" if supported else "⚠ 不支持",
            "启用 JS" if supported else "当前 Firefox 未实现",
        )

        # 10) scrollbar
        supported = page.emulation.set_scrollbar_type("overlay")
        rows.add(
            "滚动条类型",
            "✓ 通过" if supported else "⚠ 不支持",
            "overlay" if supported else "当前 Firefox 未实现",
        )

        # 11) forced colors
        supported = page.emulation.set_forced_colors_mode("dark")
        rows.add(
            "强制颜色模式",
            "✓ 通过" if supported else "⚠ 不支持",
            "dark" if supported else "当前 Firefox 未实现",
        )

        # 12) bypass csp
        supported = page.emulation.set_bypass_csp(True)
        rows.add(
            "CSP 绕过",
            "✓ 通过" if supported else "⚠ 不支持",
            "enabled=True" if supported else "当前 Firefox 未实现",
        )

        # 13) mobile preset
        support = page.emulation.apply_mobile_preset(
            custom_ua,
            width=390,
            height=844,
            device_pixel_ratio=3.0,
            orientation_type="portrait-primary",
            angle=0,
            locale="en-US",
            timezone_id="America/New_York",
            touch=True,
        )
        rows.add("移动端预设", "✓ 通过", str(support))

        print("\n" + "=" * 70)
        print("✓ Emulation 模块测试完成")
        print("=" * 70)
    finally:
        rows.table()
        page.quit()


if __name__ == "__main__":
    test_emulation()

移动端模拟访问 Google

examples/21_mobile_google_emulation.py
GitHub 仓库 已同步源码 查看 GitHub 源码
emulation advanced
一句话定位

把移动端预设能力放到真实站点中验证,是 mobile 页的主示例。

涉及 API
page.emulation.apply_mobile_presetpage.emulation.set_touch_enabledpage.getpage.run_jspage.title
展示的不同用法
  • 使用 iPhone 风格 UA 和视口访问 Google
  • 设置 locale 与 timezone
  • 检查 UA、innerWidth、devicePixelRatio
  • 验证移动页面是否正常加载
适用场景

适合 mobile、emulation 和 anti-bot 展示。

相关文档页面
推荐阅读
emulation-21google-showcase
完整代码

来源:app/content/ruyipage_examples/21_mobile_google_emulation.py · 仓库文件:21_mobile_google_emulation.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 移动端模拟访问 Google
# 说明: 把移动端预设能力放到真实站点中验证,是 mobile 页的主示例。
# 你会学到:
#   1. 使用 iPhone 风格 UA 和视口访问 Google
#   2. 设置 locale 与 timezone
#   3. 检查 UA、innerWidth、devicePixelRatio
#   4. 验证移动页面是否正常加载
# 相关 API: page.emulation.apply_mobile_preset, page.emulation.set_touch_enabled, page.get, page.run_js, page.title
# =============================

# -*- coding: utf-8 -*-
"""示例21-Mobile: 移动端模拟访问 Google

目标:
1) 使用 RuyiPage 的移动端模拟接口
2) 访问 Google 首页
3) 验证 UA / 视口 / 页面标题

说明:
- 当前 Firefox 对 emulation.setTouchOverride 未实现,因此触摸支持可能显示为 False。
- 本示例重点验证“移动端参数模拟 + 页面访问”整体链路。
"""

import io
import sys


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 test_mobile_google_emulation():
    print("=" * 70)
    print("测试: 移动端模拟访问 Google")
    print("=" * 70)

    opts = FirefoxOptions()
    opts.headless(False)
    page = FirefoxPage(opts)

    try:
        iphone_ua = (
            "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
            "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 "
            "Mobile/15E148 Safari/604.1"
        )

        print("\n1. 应用移动端模拟参数...")
        support = page.emulation.apply_mobile_preset(
            iphone_ua,
            width=390,
            height=844,
            device_pixel_ratio=3.0,
            orientation_type="portrait-primary",
            angle=0,
            locale="en-US",
            timezone_id="America/Los_Angeles",
            touch=False,
        )
        print(f"   基础支持情况: {support}")

        touch_results = {}
        for scope in ("context", "global", "user_context"):
            try:
                ok = page.emulation.set_touch_enabled(
                    True, max_touch_points=5, scope=scope
                )
            except Exception as e:
                ok = False
                print(f"   触摸作用域 {scope} 异常: {e}")
            page.refresh()
            touch_points = page.run_js("navigator.maxTouchPoints || 0")
            touch_results[scope] = {"supported": ok, "maxTouchPoints": touch_points}
        print(f"   触摸作用域结果: {touch_results}")

        print("\n2. 打开 Google 首页...")
        page.get("https://www.google.com/ncr")
        page.wait(2)

        print("\n3. 读取移动端环境信息...")
        ua = page.run_js("navigator.userAgent")
        width = page.run_js("window.innerWidth")
        height = page.run_js("window.innerHeight")
        dpr = page.run_js("window.devicePixelRatio")
        language = page.run_js("navigator.language")
        touch_points = page.run_js("navigator.maxTouchPoints || 0")
        title = page.title

        print(f"   页面标题: {title}")
        print(f"   UA: {ua}")
        print(f"   视口: {width}x{height}")
        print(f"   DPR: {dpr}")
        print(f"   语言: {language}")
        print(f"   maxTouchPoints: {touch_points}")

        ua_ok = "iPhone" in str(ua)
        viewport_ok = int(width) <= 500
        title_ok = "Google" in str(title)

        print("\n4. 结果判断...")
        print(f"   UA 命中移动端: {ua_ok}")
        print(f"   视口为移动端宽度: {viewport_ok}")
        print(f"   Google 页面打开成功: {title_ok}")

        if not (ua_ok and viewport_ok and title_ok):
            raise RuntimeError("移动端模拟访问 Google 的关键检查未全部通过")

        print("\n" + "=" * 70)
        print("✓ 移动端模拟访问 Google 测试通过")
        print("=" * 70)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        page.wait(1)
        page.quit()


if __name__ == "__main__":
    test_mobile_google_emulation()

WebExtension:目录扩展与 XPI 安装卸载

examples/22_web_extension.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser strict
一句话定位

覆盖扩展安装、验证注入和卸载,是浏览器级能力的重要专题。

涉及 API
page.extensions.installpage.extensions.install_archivepage.extensions.uninstallpage.extensions.uninstall_allpage.run_js
展示的不同用法
  • 安装目录扩展
  • 验证 content script 生效
  • 打包并安装 XPI 扩展
  • 卸载指定扩展与清空测试扩展
适用场景

适合 extensions 页面。

相关文档页面
推荐阅读
browser-user-context-25
完整代码

来源:app/content/ruyipage_examples/22_web_extension.py · 仓库文件:22_web_extension.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: WebExtension:目录扩展与 XPI 安装卸载
# 说明: 覆盖扩展安装、验证注入和卸载,是浏览器级能力的重要专题。
# 你会学到:
#   1. 安装目录扩展
#   2. 验证 content script 生效
#   3. 打包并安装 XPI 扩展
#   4. 卸载指定扩展与清空测试扩展
# 相关 API: page.extensions.install, page.extensions.install_archive, page.extensions.uninstall, page.extensions.uninstall_all, page.run_js
# =============================

# -*- coding: utf-8 -*-
"""示例22: WebExtension 模块测试(完整链路版)

完整链路:
1) 创建测试扩展
2) 安装目录扩展
3) 验证扩展已安装
4) 卸载目录扩展
5) 打包 xpi 并安装
6) 验证扩展已安装
7) 卸载打包扩展
8) 清理测试文件

说明:
- WebExtension BiDi 规范只提供 install / uninstall 命令。
- 当前 Firefox 环境下若返回 unknown command,则视为“当前版本不支持”,不是脚本错误。
"""

import io
import json
import os
import shutil
import sys
import zipfile

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 FirefoxPage


class Rows:
    def __init__(self):
        self.rows = []

    def add(self, name, status, detail=""):
        self.rows.append((name, status, detail))
        print(f"  {status}: {name}")
        if detail:
            print(f"     详情: {detail}")

    def summary(self):
        print("\n" + "=" * 70)
        print("测试结果汇总")
        print("=" * 70)
        print(f"{'序号':<5} {'项目':<28} {'状态':<8} {'说明'}")
        print("-" * 70)
        for i, (name, status, detail) in enumerate(self.rows, 1):
            print(f"{i:<5} {name:<28} {status:<8} {detail[:36]}")
        print("-" * 70)


def _create_test_extension(ext_dir):
    os.makedirs(ext_dir, exist_ok=True)
    manifest = {
        "manifest_version": 2,
        "name": "RuyiPage Test Extension",
        "version": "1.0.0",
        "description": "测试扩展",
        "browser_specific_settings": {
            "gecko": {
                "id": "[email protected]",
                "strict_min_version": "109.0",
            }
        },
        "background": {"scripts": ["background.js"]},
        "content_scripts": [
            {
                "matches": ["<all_urls>"],
                "js": ["content.js"],
                "run_at": "document_end",
            }
        ],
        "permissions": [],
    }
    with open(os.path.join(ext_dir, "manifest.json"), "w", encoding="utf-8") as f:
        json.dump(manifest, f, indent=2, ensure_ascii=False)

    with open(os.path.join(ext_dir, "background.js"), "w", encoding="utf-8") as f:
        f.write("console.log('RuyiPage Test Extension loaded!');")

    with open(os.path.join(ext_dir, "content.js"), "w", encoding="utf-8") as f:
        f.write(
            "document.documentElement.setAttribute('data-ruyi-extension', 'loaded');"
        )


def _page_has_extension_marker(page):
    return (
        page.run_js(
            "return document.documentElement.getAttribute('data-ruyi-extension')"
        )
        == "loaded"
    )


def _safe_install(page, path, rows, label):
    try:
        ext_id = page.extensions.install(path)
        if ext_id:
            rows.add(label, "✓ 通过", f"extension_id={ext_id}")
            return ext_id
        rows.add(label, "⚠ 不支持", "当前 Firefox 未返回 extension id")
        return None
    except Exception as e:
        msg = str(e).lower()
        if "unknown command" in msg or "not supported" in msg:
            rows.add(label, "⚠ 不支持", "当前 Firefox 不支持 webExtension.install")
            return None
        raise


def test_web_extension():
    print("=" * 70)
    print("测试 22: WebExtension 模块")
    print("=" * 70)

    page = FirefoxPage()
    rows = Rows()
    ext_dir = os.path.join(os.path.dirname(__file__), "test_extension")
    xpi_path = os.path.join(os.path.dirname(__file__), "test_extension.xpi")

    ext_id = None
    ext_id2 = None

    try:
        # 1) 创建测试扩展
        _create_test_extension(ext_dir)
        rows.add("创建测试扩展", "✓ 通过", ext_dir)

        # 2) 安装目录扩展
        ext_id = _safe_install(page, ext_dir, rows, "安装目录扩展")

        # 3) 验证目录扩展已生效
        if ext_id:
            page.get("https://example.com")
            page.wait(1.5)
            marker = _page_has_extension_marker(page)
            rows.add(
                "目录扩展生效验证",
                "✓ 通过" if marker else "✗ 失败",
                "content script 已注入" if marker else "未检测到注入标记",
            )
        else:
            rows.add("目录扩展生效验证", "⚠ 跳过", "安装未成功")

        # 4) 卸载目录扩展
        if ext_id:
            page.extensions.uninstall(ext_id)
            rows.add("卸载目录扩展", "✓ 通过")
        else:
            rows.add("卸载目录扩展", "⚠ 跳过", "未安装成功")

        # 5) 打包 xpi
        with zipfile.ZipFile(xpi_path, "w", zipfile.ZIP_DEFLATED) as zipf:
            zipf.write(os.path.join(ext_dir, "manifest.json"), "manifest.json")
            zipf.write(os.path.join(ext_dir, "background.js"), "background.js")
            zipf.write(os.path.join(ext_dir, "content.js"), "content.js")
        rows.add("打包 XPI", "✓ 通过", xpi_path)

        # 6) 安装打包扩展
        try:
            ext_id2 = page.extensions.install_archive(xpi_path)
            if ext_id2:
                rows.add("安装 XPI 扩展", "✓ 通过", f"extension_id={ext_id2}")
            else:
                rows.add(
                    "安装 XPI 扩展", "⚠ 不支持", "当前 Firefox 未返回 extension id"
                )
        except Exception as e:
            msg = str(e).lower()
            if "unknown command" in msg or "not supported" in msg:
                rows.add(
                    "安装 XPI 扩展",
                    "⚠ 不支持",
                    "当前 Firefox 不支持 webExtension.install",
                )
            else:
                raise

        # 7) 验证 XPI 扩展已生效
        if ext_id2:
            page.get("https://example.com")
            page.wait(1.5)
            marker = _page_has_extension_marker(page)
            rows.add(
                "XPI 扩展生效验证",
                "✓ 通过" if marker else "✗ 失败",
                "content script 已注入" if marker else "未检测到注入标记",
            )
        else:
            rows.add("XPI 扩展生效验证", "⚠ 跳过", "安装未成功")

        # 8) 卸载打包扩展
        if ext_id2:
            page.extensions.uninstall(ext_id2)
            rows.add("卸载 XPI 扩展", "✓ 通过")
        else:
            rows.add("卸载 XPI 扩展", "⚠ 跳过", "未安装成功")

        print("\n" + "=" * 70)
        print("✓ WebExtension 模块测试完成")
        print("=" * 70)

    except Exception as e:
        rows.add("WebExtension 模块", "✗ 失败", str(e))
        import traceback

        traceback.print_exc()
    finally:
        try:
            page.extensions.uninstall_all()
        except Exception:
            pass
        page.quit()

        if os.path.exists(ext_dir):
            shutil.rmtree(ext_dir)
        if os.path.exists(xpi_path):
            os.remove(xpi_path)

        rows.add("清理测试文件", "✓ 通过")
        rows.summary()


if __name__ == "__main__":
    test_web_extension()

下载管理:下载目录、事件订阅、等待落盘、禁止下载

examples/23_download.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser strict
一句话定位

讲清下载事件链和文件落盘验证,是 downloads 页的主示例。

涉及 API
page.downloads.set_behaviorpage.downloads.startpage.downloads.wait_chainpage.downloads.waitpage.downloads.wait_filepage.downloads.stop
展示的不同用法
  • 设置 allow 下载行为并指定目录
  • 订阅下载开始和结束事件
  • 等待文本和 JSON 文件下载完成
  • 验证文件已落盘
  • 切换 deny 模式阻止下载
适用场景

适合 downloads 与 events 页面。

相关文档页面
推荐阅读
pdf-19collector-28
完整代码

来源:app/content/ruyipage_examples/23_download.py · 仓库文件:23_download.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 下载管理:下载目录、事件订阅、等待落盘、禁止下载
# 说明: 讲清下载事件链和文件落盘验证,是 downloads 页的主示例。
# 你会学到:
#   1. 设置 allow 下载行为并指定目录
#   2. 订阅下载开始和结束事件
#   3. 等待文本和 JSON 文件下载完成
#   4. 验证文件已落盘
#   5. 切换 deny 模式阻止下载
# 相关 API: page.downloads.set_behavior, page.downloads.start, page.downloads.wait_chain, page.downloads.wait, page.downloads.wait_file, page.downloads.stop
# =============================

# -*- coding: utf-8 -*-
"""示例23: 下载管理测试(严格结果版)"""

import io
import os
import shutil
import sys
from typing import List, Dict

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 FirefoxPage
from ruyipage._functions.tools import find_free_port

from test_server import TestServer


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 build_download_page(base_url: str) -> str:
    """构造下载测试页。"""
    return """
    <!DOCTYPE html>
    <html>
    <head><meta charset='utf-8'><title>下载测试</title></head>
    <body>
        <button id='download-text'>下载文本文件</button>
        <button id='download-json'>下载JSON文件</button>
        <div id='status'></div>
        <script>
            function triggerDownload(url, statusText) {
                const a = document.createElement('a');
                a.href = url;
                a.click();
                document.getElementById('status').textContent = statusText;
            }

            document.getElementById('download-text').onclick = () => {
                triggerDownload('%s/download/text', '文本文件下载已触发');
            };

            document.getElementById('download-json').onclick = () => {
                triggerDownload('%s/download/json', 'JSON文件下载已触发');
            };
        </script>
    </body>
    </html>
    """ % (base_url, base_url)


def main() -> None:
    print("=" * 70)
    print("测试 23: Download 下载管理测试")
    print("=" * 70)

    page = FirefoxPage()
    server = None
    results: List[Dict[str, str]] = []
    download_path = os.path.abspath(
        os.path.join(os.path.dirname(__file__), "downloads")
    )
    text_path = os.path.join(download_path, "test.txt")
    json_path = os.path.join(download_path, "test.json")

    try:
        if os.path.exists(download_path):
            shutil.rmtree(download_path)
        os.makedirs(download_path, exist_ok=True)

        server = TestServer(port=find_free_port(8930, 9030)).start()
        base_url = server.get_url("")[:-1]

        # 1. 允许下载
        try:
            page.downloads.set_behavior("allow", path=download_path)
            add_result(
                results,
                "browser.setDownloadBehavior allow",
                "成功",
                f"下载目录: {download_path}",
            )
        except Exception as e:
            add_result(
                results, "browser.setDownloadBehavior allow", "失败", str(e)[:120]
            )
            raise

        # 2. 加载页面
        page.get("data:text/html;charset=utf-8," + build_download_page(base_url))
        add_result(
            results, "下载测试页加载", "成功", f"下载页已加载,服务地址: {base_url}"
        )

        # 3. 订阅下载事件
        subscribed = page.downloads.start()
        if subscribed:
            add_result(
                results,
                "下载事件订阅",
                "成功",
                "已订阅 downloadWillBegin / downloadEnd",
            )
        else:
            add_result(
                results, "下载事件订阅", "不支持", "当前浏览器未成功订阅下载事件"
            )

        # 4. 文本文件下载
        page.downloads.clear()
        page.ele("#download-text").click_self()
        begin_text, end_text = page.downloads.wait_chain(filename="test.txt", timeout=5)

        if begin_text:
            add_result(
                results,
                "downloadWillBegin text",
                "成功",
                f"filename={begin_text.suggested_filename or 'unknown'}",
            )
        else:
            add_result(
                results, "downloadWillBegin text", "失败", "5 秒内未收到开始事件"
            )

        if end_text:
            end_status = end_text.status or "unknown"
            add_result(results, "downloadEnd text", "成功", f"status={end_status}")
        else:
            add_result(results, "downloadEnd text", "失败", "5 秒内未收到结束事件")

        if page.downloads.wait_file(text_path, timeout=3):
            add_result(
                results, "text 文件落盘", "成功", f"{os.path.getsize(text_path)} bytes"
            )
        else:
            add_result(results, "text 文件落盘", "失败", "事件触发后仍未观察到文件落盘")

        # 5. JSON 文件下载
        page.downloads.clear()
        page.ele("#download-json").click_self()
        begin_json, end_json = page.downloads.wait_chain(
            filename="test.json", timeout=5
        )

        if begin_json:
            add_result(
                results,
                "downloadWillBegin json",
                "成功",
                f"filename={begin_json.suggested_filename or 'unknown'}",
            )
        else:
            add_result(
                results, "downloadWillBegin json", "失败", "5 秒内未收到开始事件"
            )

        if end_json:
            end_status = end_json.status or "unknown"
            add_result(results, "downloadEnd json", "成功", f"status={end_status}")
        else:
            add_result(results, "downloadEnd json", "失败", "5 秒内未收到结束事件")

        if page.downloads.wait_file(json_path, timeout=3):
            add_result(
                results, "json 文件落盘", "成功", f"{os.path.getsize(json_path)} bytes"
            )
        else:
            add_result(results, "json 文件落盘", "失败", "事件触发后仍未观察到文件落盘")

        # 6. deny 模式
        page.downloads.clear()
        try:
            page.downloads.set_behavior("deny")
            page.ele("#download-text").click_self()
            denied_begin = page.downloads.wait(
                method="browsingContext.downloadWillBegin",
                timeout=2,
                filename="test.txt",
            )
            if denied_begin:
                add_result(
                    results,
                    "browser.setDownloadBehavior deny",
                    "失败",
                    "deny 模式下仍出现下载开始事件",
                )
            else:
                add_result(
                    results,
                    "browser.setDownloadBehavior deny",
                    "成功",
                    "deny 模式下未观察到下载开始事件",
                )
        except Exception as e:
            add_result(
                results, "browser.setDownloadBehavior deny", "失败", str(e)[:120]
            )

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.downloads.stop()
        except Exception:
            pass
        try:
            if server is not None:
                server.stop()
        except Exception:
            pass
        try:
            page.quit()
        except Exception:
            pass
        if os.path.exists(download_path):
            shutil.rmtree(download_path)


if __name__ == "__main__":
    main()

导航事件:started、load、fragment、history、committed、failed

examples/24_navigation_events.py
GitHub 仓库 已同步源码 查看 GitHub 源码
network strict
一句话定位

导航事件链路的标准参考示例。

涉及 API
page.navigation.startpage.navigation.waitpage.navigation.wait_for_loadpage.navigation.wait_for_fragmentpage.navigation.stop
展示的不同用法
  • 订阅导航生命周期事件
  • 观察 navigationStarted、DOMContentLoaded 和 load
  • 观察 hash 片段跳转
  • 观察 History API 更新
  • 构造失败导航场景
适用场景

适合 navigation-events 页面。

推荐阅读
network-events-31browsing-events-30
完整代码

来源:app/content/ruyipage_examples/24_navigation_events.py · 仓库文件:24_navigation_events.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 导航事件:started、load、fragment、history、committed、failed
# 说明: 导航事件链路的标准参考示例。
# 你会学到:
#   1. 订阅导航生命周期事件
#   2. 观察 navigationStarted、DOMContentLoaded 和 load
#   3. 观察 hash 片段跳转
#   4. 观察 History API 更新
#   5. 构造失败导航场景
# 相关 API: page.navigation.start, page.navigation.wait, page.navigation.wait_for_load, page.navigation.wait_for_fragment, page.navigation.stop
# =============================

# -*- coding: utf-8 -*-
"""示例24: Navigation Events 导航事件测试(严格结果版)"""

import io
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 FirefoxPage
from ruyipage._functions.tools import find_free_port

from test_server import TestServer


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("测试 24: Navigation Events 导航事件测试")
    print("=" * 70)

    page = FirefoxPage()
    server = None
    results: List[Dict[str, str]] = []

    try:
        server = TestServer(port=find_free_port(9030, 9130)).start()
        base_url = server.get_url("")[:-1]

        tracked_events = [
            "browsingContext.navigationStarted",
            "browsingContext.fragmentNavigated",
            "browsingContext.historyUpdated",
            "browsingContext.domContentLoaded",
            "browsingContext.load",
            "browsingContext.navigationCommitted",
            "browsingContext.navigationFailed",
        ]
        if not page.navigation.start(tracked_events):
            add_result(results, "导航事件订阅", "失败", "当前环境订阅导航事件失败")
            print_results(results)
            return

        # 1. 基础导航事件
        page.navigation.clear()
        page.get(base_url + "/nav/basic")
        started = page.navigation.wait("browsingContext.navigationStarted", timeout=3)
        dom_loaded = page.navigation.wait("browsingContext.domContentLoaded", timeout=3)
        loaded = page.navigation.wait_for_load(timeout=3)

        if started and (started.url or "").endswith("/nav/basic"):
            add_result(results, "navigationStarted", "成功", "收到基础导航开始事件")
        else:
            add_result(
                results, "navigationStarted", "失败", "未观察到基础页面导航开始事件"
            )

        if dom_loaded and dom_loaded.context == page.tab_id:
            add_result(
                results, "domContentLoaded", "成功", "收到 DOMContentLoaded 事件"
            )
        else:
            add_result(
                results, "domContentLoaded", "失败", "未观察到 DOMContentLoaded 事件"
            )

        if loaded and loaded.context == page.tab_id:
            add_result(results, "load", "成功", "收到 load 事件")
        else:
            add_result(results, "load", "失败", "未观察到 load 事件")

        # 2. fragmentNavigated
        page.get(base_url + "/nav/fragment")
        page.navigation.clear()
        page.run_js(
            """
            location.hash = '#a';
            setTimeout(function () { location.hash = '#b'; }, 100);
            """,
            as_expr=False,
        )
        first_fragment = page.navigation.wait_for_fragment("a", timeout=3)
        second_fragment = page.navigation.wait_for_fragment("b", timeout=3)

        if first_fragment and second_fragment:
            add_result(
                results,
                "fragmentNavigated",
                "成功",
                f"两次片段导航已触发: {first_fragment.url} -> {second_fragment.url}",
            )
        else:
            add_result(
                results,
                "fragmentNavigated",
                "不支持",
                "片段 URL 已变化,但当前 Firefox 未观察到标准 fragmentNavigated 事件",
            )

        # 3. historyUpdated
        page.get(base_url + "/nav/history")
        page.navigation.clear()
        page.run_js(
            """
            history.pushState({p:1}, 'P1', '?p=1');
            history.pushState({p:2}, 'P2', '?p=2');
            history.back();
            """,
            as_expr=False,
        )
        first_history = page.navigation.wait(
            "browsingContext.historyUpdated", timeout=3
        )
        second_history = page.navigation.wait(
            "browsingContext.historyUpdated", timeout=3
        )

        if first_history or second_history:
            add_result(
                results, "historyUpdated", "成功", "pushState/back 触发 historyUpdated"
            )
        else:
            add_result(
                results, "historyUpdated", "失败", "未观察到 historyUpdated 事件"
            )

        # 4. navigationCommitted
        page.navigation.clear()
        page.get(base_url + "/nav/basic?committed=1")
        committed = page.navigation.wait(
            "browsingContext.navigationCommitted", timeout=3
        )
        if committed and committed.context == page.tab_id:
            add_result(
                results,
                "navigationCommitted",
                "成功",
                "普通导航触发 navigationCommitted",
            )
        else:
            add_result(
                results,
                "navigationCommitted",
                "跳过",
                "当前环境未稳定观察到 navigationCommitted",
            )

        # 5. navigationFailed
        page.navigation.clear()
        try:
            page.get("http://127.0.0.1:9/", timeout=2)
        except Exception:
            pass
        failed = page.navigation.wait("browsingContext.navigationFailed", timeout=3)
        if failed:
            add_result(
                results, "navigationFailed", "成功", "不可达地址触发 navigationFailed"
            )
        else:
            add_result(
                results,
                "navigationFailed",
                "跳过",
                "当前环境未稳定观察到 navigationFailed",
            )

        # 6. navigationAborted
        if page.navigation.start(["browsingContext.navigationAborted"]):
            add_result(
                results,
                "navigationAborted",
                "跳过",
                "订阅成功,但本示例未构造稳定 aborted 场景",
            )
        else:
            add_result(
                results, "navigationAborted", "不支持", "当前 Firefox 不接受该事件名"
            )

        # 恢复默认跟踪,便于 finally 统一 stop
        page.navigation.start(tracked_events)

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.navigation.stop()
        except Exception:
            pass
        try:
            if server is not None:
                server.stop()
        except Exception:
            pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

Browser 模块:用户上下文与客户端窗口

examples/25_browser_user_context.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser strict
一句话定位

Browser 模块最完整的 user context 与 client window 说明案例。

涉及 API
page.browser_tools.get_user_contextspage.browser_tools.create_user_contextpage.browser_tools.remove_user_contextpage.browser_tools.create_tabpage.browser_tools.get_client_windowspage.browser_tools.set_window_state
展示的不同用法
  • 读取 user context 列表
  • 创建与删除 user context
  • 在指定 user context 中创建新 tab
  • 读取 client window 列表
  • 切换窗口状态为 minimized / maximized / fullscreen
适用场景

适合 browser、user-contexts 和 window 页面。

相关文档页面
推荐阅读
contexts-26window-16
完整代码

来源:app/content/ruyipage_examples/25_browser_user_context.py · 仓库文件:25_browser_user_context.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: Browser 模块:用户上下文与客户端窗口
# 说明: Browser 模块最完整的 user context 与 client window 说明案例。
# 你会学到:
#   1. 读取 user context 列表
#   2. 创建与删除 user context
#   3. 在指定 user context 中创建新 tab
#   4. 读取 client window 列表
#   5. 切换窗口状态为 minimized / maximized / fullscreen
# 相关 API: page.browser_tools.get_user_contexts, page.browser_tools.create_user_context, page.browser_tools.remove_user_context, page.browser_tools.create_tab, page.browser_tools.get_client_windows, page.browser_tools.set_window_state
# =============================

# -*- coding: utf-8 -*-
"""示例25: Browser 模块(用户上下文 + 客户端窗口)严格结果版"""

import io
import sys
import time
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 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("测试 25: Browser 用户上下文与窗口管理")
    print("=" * 70)

    page = FirefoxPage()
    results: List[Dict[str, str]] = []
    user_context_id = None
    new_tab_ctx = None

    try:
        page.get("https://www.example.com")

        # 1. user context 生命周期
        before = page.browser_tools.get_user_contexts()
        before_ids = [i.get("userContext") for i in before]
        add_result(
            results,
            "browser.getUserContexts before",
            "成功",
            f"创建前数量: {len(before)}",
        )

        user_context_id = page.browser_tools.create_user_context()
        if user_context_id:
            add_result(
                results,
                "browser.createUserContext",
                "成功",
                f"userContext={user_context_id}",
            )
        else:
            add_result(
                results, "browser.createUserContext", "失败", "未返回 userContext ID"
            )

        after_create = page.browser_tools.get_user_contexts()
        after_create_ids = [i.get("userContext") for i in after_create]
        if user_context_id in after_create_ids and len(after_create) == len(before) + 1:
            add_result(
                results,
                "browser.getUserContexts after create",
                "成功",
                f"数量变为 {len(after_create)}",
            )
        else:
            add_result(
                results,
                "browser.getUserContexts after create",
                "失败",
                "创建后数量或ID校验不通过",
            )

        if user_context_id:
            new_tab_ctx = page.browser_tools.create_tab(user_context=user_context_id)
            if new_tab_ctx:
                add_result(
                    results,
                    "browsingContext.create tab in user context",
                    "成功",
                    f"context={new_tab_ctx}",
                )
            else:
                add_result(
                    results,
                    "browsingContext.create tab in user context",
                    "失败",
                    "未返回新 tab context",
                )

        if new_tab_ctx:
            try:
                page.contexts.close(new_tab_ctx)
                add_result(
                    results, "browsingContext.close new tab", "成功", "新 tab 已关闭"
                )
                new_tab_ctx = None
            except Exception as e:
                add_result(
                    results, "browsingContext.close new tab", "跳过", str(e)[:120]
                )

        if user_context_id:
            page.browser_tools.remove_user_context(user_context_id)
            add_result(
                results,
                "browser.removeUserContext",
                "成功",
                f"已删除 {user_context_id}",
            )

            after_remove = page.browser_tools.get_user_contexts()
            after_remove_ids = [i.get("userContext") for i in after_remove]
            if user_context_id not in after_remove_ids and len(after_remove) == len(
                before_ids
            ):
                add_result(
                    results,
                    "browser.getUserContexts after remove",
                    "成功",
                    f"数量回到 {len(after_remove)}",
                )
            else:
                add_result(
                    results,
                    "browser.getUserContexts after remove",
                    "失败",
                    "删除后数量或ID校验不通过",
                )
            user_context_id = None

        # 2. client window 状态流
        windows = page.browser_tools.get_client_windows()
        if not windows:
            add_result(
                results, "browser.getClientWindows", "失败", "未返回任何 client window"
            )
        else:
            add_result(
                results, "browser.getClientWindows", "成功", f"窗口数量: {len(windows)}"
            )
            win = windows[0]
            win_id = win.get("clientWindow")
            states = ["minimized", "normal", "maximized", "fullscreen", "normal"]
            state_notes = []
            state_failed = False
            for state in states:
                page.browser_tools.set_window_state(win_id, state=state)
                time.sleep(0.4)
                current_windows = page.browser_tools.get_client_windows()
                current = current_windows[0] if current_windows else {}
                current_state = current.get("state")
                state_notes.append(f"{state}->{current_state}")
                if current_state != state:
                    state_failed = True

            if state_failed:
                add_result(
                    results,
                    "browser.setClientWindowState",
                    "失败",
                    ", ".join(state_notes),
                )
            else:
                add_result(
                    results,
                    "browser.setClientWindowState",
                    "成功",
                    ", ".join(state_notes),
                )

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        if new_tab_ctx:
            try:
                page.contexts.close(new_tab_ctx)
            except Exception:
                pass
        if user_context_id:
            try:
                page.browser_tools.remove_user_context(user_context_id)
            except Exception:
                pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

BrowsingContext 高级能力:树、创建、关闭、reload、viewport

examples/26_browsing_context_advanced.py
GitHub 仓库 已同步源码 查看 GitHub 源码
browser strict
一句话定位

是 browsing context 章节的主参考示例。

涉及 API
page.contexts.get_treepage.contexts.create_tabpage.contexts.closepage.contexts.reloadpage.contexts.set_bypass_csppage.contexts.set_viewport
展示的不同用法
  • 获取 browsing context 树
  • 通过标准命令创建和关闭 context
  • 执行普通 reload 和 ignore_cache reload
  • 设置 bypass CSP
  • 调整桌面和移动视口尺寸
适用场景

适合 browsing-context 页面。

推荐阅读
browser-user-context-25tabs-09
完整代码

来源:app/content/ruyipage_examples/26_browsing_context_advanced.py · 仓库文件:26_browsing_context_advanced.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: BrowsingContext 高级能力:树、创建、关闭、reload、viewport
# 说明: 是 browsing context 章节的主参考示例。
# 你会学到:
#   1. 获取 browsing context 树
#   2. 通过标准命令创建和关闭 context
#   3. 执行普通 reload 和 ignore_cache reload
#   4. 设置 bypass CSP
#   5. 调整桌面和移动视口尺寸
# 相关 API: page.contexts.get_tree, page.contexts.create_tab, page.contexts.close, page.contexts.reload, page.contexts.set_bypass_csp, page.contexts.set_viewport
# =============================

# -*- coding: utf-8 -*-
"""示例26: BrowsingContext 高级功能(严格结果版)"""

import io
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 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("测试 26: BrowsingContext 高级功能")
    print("=" * 70)

    page = FirefoxPage()
    child_id = None
    results: List[Dict[str, str]] = []

    try:
        page.get("https://www.example.com")
        add_result(results, "页面加载", "成功", "example.com 已加载")

        tree = page.contexts.get_tree()
        contexts = tree.contexts
        if contexts:
            add_result(
                results,
                "browsingContext.getTree",
                "成功",
                f"上下文数量: {len(contexts)}",
            )
        else:
            add_result(results, "browsingContext.getTree", "失败", "未返回任何上下文")

        child_id = page.contexts.create_tab()
        if child_id:
            add_result(
                results, "browsingContext.create", "成功", f"新 context: {child_id}"
            )
            page.contexts.close(child_id)
            add_result(results, "browsingContext.close", "成功", "新 context 已关闭")
            child_id = None
        else:
            add_result(results, "browsingContext.create", "失败", "未返回 context ID")

        r1 = page.contexts.reload()
        if r1.get("navigation"):
            add_result(
                results,
                "browsingContext.reload",
                "成功",
                f"navigation={r1.get('navigation')}",
            )
        else:
            add_result(
                results, "browsingContext.reload", "跳过", "当前返回未包含 navigation"
            )

        try:
            r2 = page.contexts.reload(ignore_cache=True)
            add_result(
                results,
                "browsingContext.reload ignoreCache",
                "成功",
                f"navigation={r2.get('navigation')}",
            )
        except Exception as e:
            add_result(
                results, "browsingContext.reload ignoreCache", "不支持", str(e)[:120]
            )

        try:
            page.contexts.set_bypass_csp(True)
            add_result(
                results, "browsingContext.setBypassCSP", "成功", "标准命令调用成功"
            )
        except Exception as e:
            add_result(results, "browsingContext.setBypassCSP", "不支持", str(e)[:120])

        page.contexts.set_viewport(800, 600)
        page.wait(0.2)
        vp = page.rect.viewport_size
        if tuple(vp) == (800, 600):
            add_result(
                results,
                "browsingContext.setViewport 800x600",
                "成功",
                f"实际视口: {vp}",
            )
        else:
            add_result(
                results,
                "browsingContext.setViewport 800x600",
                "失败",
                f"实际视口: {vp}",
            )

        page.contexts.set_viewport(375, 667)
        page.wait(0.2)
        vp2 = page.rect.viewport_size
        if tuple(vp2) == (375, 667):
            add_result(
                results,
                "browsingContext.setViewport 375x667",
                "成功",
                f"实际视口: {vp2}",
            )
        else:
            add_result(
                results,
                "browsingContext.setViewport 375x667",
                "失败",
                f"实际视口: {vp2}",
            )

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        if child_id:
            try:
                page.contexts.close(child_id)
            except Exception:
                pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

Emulation 高级能力:颜色模式、屏幕、方向、脚本与滚动条

examples/27_emulation_advanced.py
GitHub 仓库 已同步源码 查看 GitHub 源码
emulation strict
一句话定位

强调 emulation 的高级能力和 Firefox 当前支持情况。

涉及 API
page.emulation.set_forced_colors_modepage.emulation.set_screen_sizepage.emulation.set_screen_orientationpage.emulation.set_javascript_enabledpage.emulation.set_scrollbar_type
展示的不同用法
  • 切换 forced colors 模式
  • 调整屏幕尺寸和 DPR
  • 切换屏幕方向和角度
  • 启用或禁用 JavaScript
  • 切换滚动条显示模式
适用场景

适合 emulation 的高级能力章节。

相关文档页面
推荐阅读
emulation-21
完整代码

来源:app/content/ruyipage_examples/27_emulation_advanced.py · 仓库文件:27_emulation_advanced.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: Emulation 高级能力:颜色模式、屏幕、方向、脚本与滚动条
# 说明: 强调 emulation 的高级能力和 Firefox 当前支持情况。
# 你会学到:
#   1. 切换 forced colors 模式
#   2. 调整屏幕尺寸和 DPR
#   3. 切换屏幕方向和角度
#   4. 启用或禁用 JavaScript
#   5. 切换滚动条显示模式
# 相关 API: page.emulation.set_forced_colors_mode, page.emulation.set_screen_size, page.emulation.set_screen_orientation, page.emulation.set_javascript_enabled, page.emulation.set_scrollbar_type
# =============================

# -*- coding: utf-8 -*-
"""示例27: Emulation 高级能力(严格结果版)"""

import io
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 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("测试 27: Emulation 高级能力")
    print("=" * 70)

    page = FirefoxPage()
    results: List[Dict[str, str]] = []

    try:
        page.get("https://www.example.com")
        add_result(results, "页面加载", "成功", "example.com 已加载")

        # 1. 强制颜色模式
        forced_active = page.emulation.set_forced_colors_mode("active")
        add_result(
            results,
            "emulation.setForcedColorsModeThemeOverride active",
            "成功" if forced_active else "不支持",
            "标准命令已实现" if forced_active else "当前 Firefox 未实现该命令",
        )

        forced_none = page.emulation.set_forced_colors_mode("none")
        add_result(
            results,
            "emulation.setForcedColorsModeThemeOverride none",
            "成功" if forced_none else "不支持",
            "标准命令已实现" if forced_none else "当前 Firefox 未实现该命令",
        )

        # 2. 屏幕设置
        page.emulation.set_screen_size(1920, 1080, device_pixel_ratio=2.0)
        size1 = page.run_js(
            "return [screen.width, screen.height, window.devicePixelRatio]"
        )
        if (
            size1
            and int(size1[0]) == 1920
            and int(size1[1]) == 1080
            and float(size1[2]) == 2.0
        ):
            add_result(
                results,
                "emulation.setScreenSettingsOverride 1920x1080@2",
                "成功",
                str(size1),
            )
        elif size1 and int(size1[0]) == 1920 and int(size1[1]) == 1080:
            add_result(
                results,
                "emulation.setScreenSettingsOverride 1920x1080@2",
                "跳过",
                f"尺寸生效,但 DPR 未生效: {size1}",
            )
        else:
            add_result(
                results,
                "emulation.setScreenSettingsOverride 1920x1080@2",
                "失败",
                str(size1)[:120],
            )

        page.emulation.set_screen_size(375, 812, device_pixel_ratio=3.0)
        size2 = page.run_js(
            "return [screen.width, screen.height, window.devicePixelRatio]"
        )
        if (
            size2
            and int(size2[0]) == 375
            and int(size2[1]) == 812
            and float(size2[2]) == 3.0
        ):
            add_result(
                results,
                "emulation.setScreenSettingsOverride 375x812@3",
                "成功",
                str(size2),
            )
        elif size2 and int(size2[0]) == 375 and int(size2[1]) == 812:
            add_result(
                results,
                "emulation.setScreenSettingsOverride 375x812@3",
                "跳过",
                f"尺寸生效,但 DPR 未生效: {size2}",
            )
        else:
            add_result(
                results,
                "emulation.setScreenSettingsOverride 375x812@3",
                "失败",
                str(size2)[:120],
            )

        # 3. 屏幕方向
        page.emulation.set_screen_orientation("portrait-primary", angle=0)
        orientation1 = page.run_js(
            "return [screen.orientation.type, screen.orientation.angle]"
        )
        if orientation1 and orientation1[0] == "portrait-primary":
            add_result(
                results,
                "emulation.setScreenOrientationOverride portrait",
                "成功",
                str(orientation1),
            )
        else:
            add_result(
                results,
                "emulation.setScreenOrientationOverride portrait",
                "失败",
                str(orientation1)[:120],
            )

        page.emulation.set_screen_orientation("landscape-primary", angle=90)
        orientation2 = page.run_js(
            "return [screen.orientation.type, screen.orientation.angle]"
        )
        if (
            orientation2
            and orientation2[0] == "landscape-primary"
            and int(orientation2[1]) == 90
        ):
            add_result(
                results,
                "emulation.setScreenOrientationOverride landscape",
                "成功",
                str(orientation2),
            )
        elif orientation2 and orientation2[0] == "landscape-primary":
            add_result(
                results,
                "emulation.setScreenOrientationOverride landscape",
                "跳过",
                f"方向类型生效,但角度未生效: {orientation2}",
            )
        else:
            add_result(
                results,
                "emulation.setScreenOrientationOverride landscape",
                "失败",
                str(orientation2)[:120],
            )

        # 4. 脚本启用/禁用
        js_off = page.emulation.set_javascript_enabled(False)
        add_result(
            results,
            "emulation.setScriptingEnabled false",
            "成功" if js_off else "不支持",
            "标准命令已实现" if js_off else "当前 Firefox 未实现该命令",
        )

        js_on = page.emulation.set_javascript_enabled(True)
        add_result(
            results,
            "emulation.setScriptingEnabled true",
            "成功" if js_on else "不支持",
            "标准命令已实现" if js_on else "当前 Firefox 未实现该命令",
        )

        # 5. 滚动条类型
        sb_none = page.emulation.set_scrollbar_type("none")
        add_result(
            results,
            "emulation.setScrollbarTypeOverride none",
            "成功" if sb_none else "不支持",
            "标准命令已实现" if sb_none else "当前 Firefox 未实现该命令",
        )

        sb_standard = page.emulation.set_scrollbar_type("standard")
        add_result(
            results,
            "emulation.setScrollbarTypeOverride standard",
            "成功" if sb_standard else "不支持",
            "标准命令已实现" if sb_standard else "当前 Firefox 未实现该命令",
        )

        sb_overlay = page.emulation.set_scrollbar_type("overlay")
        add_result(
            results,
            "emulation.setScrollbarTypeOverride overlay",
            "成功" if sb_overlay else "不支持",
            "标准命令已实现" if sb_overlay else "当前 Firefox 未实现该命令",
        )

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

网络数据采集器:响应体抓取、读取与释放

examples/28_network_data_collector.py
GitHub 仓库 已同步源码 查看 GitHub 源码
network strict
一句话定位

用于说明 add_data_collector 在数据抓取和协议观测中的价值。

涉及 API
page.network.set_extra_headerspage.network.set_cache_behaviorpage.network.add_data_collectorcollector.getcollector.disowncollector.removepage.listen.start
展示的不同用法
  • 注入额外请求头
  • 设置缓存 bypass
  • 添加并使用 data collector 采集 response 数据
  • 按 request_id 读取采集结果
  • 释放采集句柄并移除 collector
适用场景

适合 data-collector 与 network 页面。

相关文档页面
推荐阅读
advanced-network-18network-events-31
完整代码

来源:app/content/ruyipage_examples/28_network_data_collector.py · 仓库文件:28_network_data_collector.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 网络数据采集器:响应体抓取、读取与释放
# 说明: 用于说明 add_data_collector 在数据抓取和协议观测中的价值。
# 你会学到:
#   1. 注入额外请求头
#   2. 设置缓存 bypass
#   3. 添加并使用 data collector 采集 response 数据
#   4. 按 request_id 读取采集结果
#   5. 释放采集句柄并移除 collector
# 相关 API: page.network.set_extra_headers, page.network.set_cache_behavior, page.network.add_data_collector, collector.get, collector.disown, collector.remove, page.listen.start
# =============================

# -*- coding: utf-8 -*-
"""示例28: Network Data Collector(严格结果版)"""

import io
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 DataCollector, DataPacket, FirefoxPage, InterceptedRequest
from ruyipage._functions.tools import find_free_port

from test_server import TestServer


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("测试 28: Network Data Collector")
    print("=" * 70)

    page = FirefoxPage()
    results: List[Dict[str, str]] = []
    collector: DataCollector | None = None
    server = None

    try:
        server = TestServer(port=find_free_port(9230, 9330)).start()
        base_url = server.get_url("")[:-1]

        page.get("about:blank")
        add_result(
            results, "页面加载", "成功", f"about:blank 已加载,服务地址: {base_url}"
        )

        page.network.set_extra_headers({"X-Test-Collector": "yes"})
        add_result(
            results, "network.setExtraHeaders", "成功", "已设置 X-Test-Collector"
        )

        try:
            page.network.set_cache_behavior("bypass")
            add_result(
                results, "network.setCacheBehavior", "成功", "缓存行为已设为 bypass"
            )
        except Exception as e:
            add_result(results, "network.setCacheBehavior", "不支持", str(e)[:120])

        collector = page.network.add_data_collector(
            ["responseCompleted"],
            data_types=["response"],
        )
        if collector.id:
            add_result(
                results, "network.addDataCollector", "成功", f"collector={collector.id}"
            )
        else:
            add_result(
                results, "network.addDataCollector", "失败", "未返回 collector ID"
            )

        page.intercept.start_requests()
        page.listen.start("/api/collector")
        page.run_js(
            f"fetch('{base_url}/api/collector').catch(()=>null); return true;",
            as_expr=False,
        )

        req: InterceptedRequest | None = page.intercept.wait(timeout=8)

        if req:
            has_header = "X-Test-Collector" in req.headers
            add_result(
                results,
                "request header injected",
                "成功" if has_header else "失败",
                f"request_id={req.request_id}, X-Test-Collector={has_header}",
            )
            req.continue_request()
        else:
            add_result(
                results,
                "request header injected",
                "失败",
                "未捕获到 beforeRequestSent 请求",
            )

        packet: DataPacket | None = page.listen.wait(timeout=8)

        if packet:
            add_result(
                results,
                "network.responseCompleted observed",
                "成功",
                f"status={packet.status} url={packet.url}",
            )
        else:
            add_result(
                results,
                "network.responseCompleted observed",
                "失败",
                "未在超时内观察到 responseCompleted",
            )

        if collector and req:
            got_response_data = False
            try:
                data = collector.get(req.request_id, data_type="response")
                if data.has_data:
                    got_response_data = True
                    add_result(
                        results,
                        "network.getData response",
                        "成功",
                        f"bytes={data.bytes} base64={'yes' if data.base64 else 'no'}",
                    )
                else:
                    add_result(
                        results, "network.getData response", "失败", f"raw={data.raw}"
                    )
            except Exception as e:
                add_result(results, "network.getData response", "失败", str(e)[:120])

            if got_response_data:
                try:
                    collector.disown(req.request_id, data_type="response")
                    add_result(
                        results, "network.disownData", "成功", "已释放 response 数据"
                    )
                except Exception as e:
                    add_result(results, "network.disownData", "失败", str(e)[:120])

                try:
                    data_after = collector.get(req.request_id, data_type="response")
                    if not data_after.has_data:
                        add_result(
                            results,
                            "network.getData after disown",
                            "成功",
                            "释放后已无可用数据",
                        )
                    else:
                        add_result(
                            results,
                            "network.getData after disown",
                            "跳过",
                            f"释放后仍可读取: {data_after.raw}",
                        )
                except Exception as e:
                    add_result(
                        results,
                        "network.getData after disown",
                        "成功",
                        f"释放后读取报错: {str(e)[:100]}",
                    )
            else:
                add_result(
                    results,
                    "network.disownData",
                    "跳过",
                    "未成功拿到 response 数据,跳过释放验证",
                )
                add_result(
                    results,
                    "network.getData after disown",
                    "跳过",
                    "未成功拿到 response 数据,跳过释放后验证",
                )

        if collector:
            try:
                collector.remove()
                add_result(
                    results,
                    "network.removeDataCollector",
                    "成功",
                    f"已移除 {collector.id}",
                )
                collector = None
            except Exception as e:
                add_result(results, "network.removeDataCollector", "失败", str(e)[:120])

        page.network.clear_extra_headers()
        add_result(results, "clear extra headers", "成功", "额外请求头已清理")

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.listen.stop()
        except Exception:
            pass
        try:
            page.intercept.stop()
        except Exception:
            pass
        try:
            if server is not None:
                server.stop()
        except Exception:
            pass
        if collector:
            try:
                collector.remove()
            except Exception:
                pass
        try:
            page.network.clear_extra_headers()
        except Exception:
            pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

Script 与 Input 高级能力:realms、handle 回收、文件输入

examples/29_script_input_advanced.py
GitHub 仓库 已同步源码 查看 GitHub 源码
debug strict
一句话定位

连接 script 与 input 的高级能力,是 script 页的重要支撑示例。

涉及 API
page.get_realmspage.eval_handlepage.disown_handlesele.input
展示的不同用法
  • 获取全部和 window 类型 realms
  • 执行脚本得到远程对象句柄
  • 回收单个和批量 handle
  • 利用 file input 上传单文件和多文件
适用场景

适合 script 和 forms 页面。

相关文档页面
推荐阅读
javascript-07script-events-32
完整代码

来源:app/content/ruyipage_examples/29_script_input_advanced.py · 仓库文件:29_script_input_advanced.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: Script 与 Input 高级能力:realms、handle 回收、文件输入
# 说明: 连接 script 与 input 的高级能力,是 script 页的重要支撑示例。
# 你会学到:
#   1. 获取全部和 window 类型 realms
#   2. 执行脚本得到远程对象句柄
#   3. 回收单个和批量 handle
#   4. 利用 file input 上传单文件和多文件
# 相关 API: page.get_realms, page.eval_handle, page.disown_handles, ele.input
# =============================

# -*- coding: utf-8 -*-
"""示例29: Script + Input 高级能力(严格结果版)"""

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 FirefoxPage, ScriptResult


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("测试 29: Script + Input 高级能力")
    print("=" * 70)

    page = FirefoxPage()
    results: List[Dict[str, str]] = []
    html_path = os.path.join(os.path.dirname(__file__), "test_file_input.html")
    file1 = os.path.join(os.path.dirname(__file__), "test1.txt")
    file2 = os.path.join(os.path.dirname(__file__), "test2.txt")

    try:
        page.get("https://www.example.com")
        add_result(results, "页面加载", "成功", "example.com 已加载")

        # 1. script.getRealms
        realms = page.get_realms()
        if realms:
            add_result(
                results, "script.getRealms all", "成功", f"realm 数量: {len(realms)}"
            )
        else:
            add_result(results, "script.getRealms all", "失败", "未返回任何 realm")

        window_realms = page.get_realms(type_="window")
        if window_realms:
            add_result(
                results,
                "script.getRealms window",
                "成功",
                f"window realm 数量: {len(window_realms)}",
            )
        else:
            add_result(
                results, "script.getRealms window", "失败", "未返回 window realm"
            )

        # 2. script.disown
        single: ScriptResult = page.eval_handle("({data: 'test', array: [1, 2, 3]})")
        if single.success and single.result.handle:
            page.disown_handles([single.result.handle])
            add_result(
                results,
                "script.disown single",
                "成功",
                f"handle={single.result.handle}",
            )
        else:
            add_result(results, "script.disown single", "跳过", "脚本结果未返回 handle")

        handles = []
        for i in range(3):
            item = page.eval_handle(f"({{id: {i}, value: 'test{i}'}})")
            if item.success and item.result.handle:
                handles.append(item.result.handle)
        if handles:
            page.disown_handles(handles)
            add_result(
                results, "script.disown batch", "成功", f"句柄数量: {len(handles)}"
            )
        else:
            add_result(results, "script.disown batch", "跳过", "未拿到可用 handle")

        # 3. input.setFiles 通过高层文件输入
        html_content = """
        <!DOCTYPE html>
        <html>
        <head><meta charset='utf-8'><title>File Input Test</title></head>
        <body>
            <input type='file' id='single-file' />
            <input type='file' id='multiple-files' multiple />
            <div id='result'></div>
            <script>
                document.getElementById('single-file').addEventListener('change', function(e) {
                    document.getElementById('result').textContent = 'Single file: ' + e.target.files[0].name;
                });
                document.getElementById('multiple-files').addEventListener('change', function(e) {
                    document.getElementById('result').textContent = 'Multiple files: ' + e.target.files.length;
                });
            </script>
        </body>
        </html>
        """

        with open(html_path, "w", encoding="utf-8") as f:
            f.write(html_content)
        with open(file1, "w", encoding="utf-8") as f:
            f.write("Test file 1 content")
        with open(file2, "w", encoding="utf-8") as f:
            f.write("Test file 2 content")

        page.get(f"file:///{html_path.replace(os.sep, '/')}")
        add_result(results, "文件测试页加载", "成功", "本地 file input 页面已加载")

        page.ele("#single-file").input(file1)
        single_text = page.ele("#result").text
        if "test1.txt" in single_text:
            add_result(results, "input.setFiles single", "成功", single_text)
        else:
            add_result(results, "input.setFiles single", "失败", single_text)

        page.ele("#multiple-files").input([file1, file2])
        multi_text = page.ele("#result").text
        if "Multiple files: 2" in multi_text:
            add_result(results, "input.setFiles multiple", "成功", multi_text)
        else:
            add_result(results, "input.setFiles multiple", "失败", multi_text)

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        for path in (file1, file2, html_path):
            try:
                if os.path.exists(path):
                    os.remove(path)
            except Exception:
                pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

BrowsingContext 事件:上下文创建销毁与用户提示框事件

examples/30_browsing_context_events.py
GitHub 仓库 已同步源码 查看 GitHub 源码
network strict
一句话定位

把 context 事件和 prompt 事件统一放到协议事件角度理解。

涉及 API
page.events.startpage.events.waitpage.contexts.create_tabpage.contexts.create_windowpage.contexts.closepage.accept_promptpage.set_prompt_handler
展示的不同用法
  • 监听 tab 和 window 创建销毁
  • 监听 alert 和 prompt 打开关闭
  • 配合自动 prompt 策略处理事件链
适用场景

适合 browsing-context、events 与 prompts 页面。

推荐阅读
navigation-events-24prompts-17
完整代码

来源:app/content/ruyipage_examples/30_browsing_context_events.py · 仓库文件:30_browsing_context_events.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: BrowsingContext 事件:上下文创建销毁与用户提示框事件
# 说明: 把 context 事件和 prompt 事件统一放到协议事件角度理解。
# 你会学到:
#   1. 监听 tab 和 window 创建销毁
#   2. 监听 alert 和 prompt 打开关闭
#   3. 配合自动 prompt 策略处理事件链
# 相关 API: page.events.start, page.events.wait, page.contexts.create_tab, page.contexts.create_window, page.contexts.close, page.accept_prompt, page.set_prompt_handler
# =============================

# -*- coding: utf-8 -*-
"""示例30: BrowsingContext Events(严格结果版)"""

import io
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 BidiEvent, 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("测试 30: BrowsingContext Events")
    print("=" * 70)

    page = FirefoxPage()
    results: List[Dict[str, str]] = []
    temp_tab = None
    temp_window = None

    try:
        page.get("https://www.example.com")
        add_result(results, "页面加载", "成功", "example.com 已加载")

        if page.events.start(
            [
                "browsingContext.contextCreated",
                "browsingContext.contextDestroyed",
                "browsingContext.userPromptOpened",
                "browsingContext.userPromptClosed",
            ],
            contexts=[],
        ):
            add_result(results, "事件订阅", "成功", "已订阅 context / prompt 相关事件")
        else:
            add_result(results, "事件订阅", "失败", "未能订阅 browsingContext 事件")
            print_results(results)
            return

        # 1. contextCreated / contextDestroyed
        page.events.clear()
        temp_tab = page.contexts.create_tab()
        created_tab: BidiEvent | None = page.events.wait(
            "browsingContext.contextCreated", timeout=3
        )
        if created_tab and created_tab.context == temp_tab:
            add_result(
                results,
                "browsingContext.contextCreated tab",
                "成功",
                f"context={temp_tab}",
            )
        else:
            add_result(
                results,
                "browsingContext.contextCreated tab",
                "失败",
                f"expected={temp_tab}",
            )

        temp_window = page.contexts.create_window()
        created_window: BidiEvent | None = page.events.wait(
            "browsingContext.contextCreated", timeout=3
        )
        if created_window and created_window.context == temp_window:
            add_result(
                results,
                "browsingContext.contextCreated window",
                "成功",
                f"context={temp_window}",
            )
        else:
            add_result(
                results,
                "browsingContext.contextCreated window",
                "失败",
                f"expected={temp_window}",
            )

        if temp_tab:
            page.contexts.close(temp_tab)
            destroyed_tab = page.events.wait(
                "browsingContext.contextDestroyed", timeout=3
            )
            if destroyed_tab and destroyed_tab.context == temp_tab:
                add_result(
                    results,
                    "browsingContext.contextDestroyed tab",
                    "成功",
                    f"context={temp_tab}",
                )
            else:
                add_result(
                    results,
                    "browsingContext.contextDestroyed tab",
                    "失败",
                    f"expected={temp_tab}",
                )
            temp_tab = None

        if temp_window:
            page.contexts.close(temp_window)
            destroyed_window = page.events.wait(
                "browsingContext.contextDestroyed", timeout=3
            )
            if destroyed_window and destroyed_window.context == temp_window:
                add_result(
                    results,
                    "browsingContext.contextDestroyed window",
                    "成功",
                    f"context={temp_window}",
                )
            else:
                add_result(
                    results,
                    "browsingContext.contextDestroyed window",
                    "失败",
                    f"expected={temp_window}",
                )
            temp_window = None

        # 2. userPromptOpened / userPromptClosed
        page.events.clear()

        page.run_js("alert('hello alert')", as_expr=False)
        opened_alert = page.events.wait("browsingContext.userPromptOpened", timeout=3)
        if opened_alert and opened_alert.user_prompt_type == "alert":
            add_result(
                results,
                "browsingContext.userPromptOpened alert",
                "成功",
                opened_alert.message or "",
            )
        else:
            add_result(
                results,
                "browsingContext.userPromptOpened alert",
                "失败",
                "未观察到 alert 打开事件",
            )

        page.accept_prompt(timeout=3)
        closed_alert = page.events.wait("browsingContext.userPromptClosed", timeout=3)
        if closed_alert and closed_alert.accepted is True:
            add_result(
                results,
                "browsingContext.userPromptClosed alert",
                "成功",
                f"accepted={closed_alert.accepted}",
            )
        else:
            add_result(
                results,
                "browsingContext.userPromptClosed alert",
                "失败",
                "未观察到 alert 关闭事件",
            )

        page.events.clear()
        page.set_prompt_handler(prompt="ignore", prompt_text="Test User")

        page.run_js("prompt('Enter your name:', 'default')", as_expr=False)
        opened_prompt = page.events.wait("browsingContext.userPromptOpened", timeout=3)
        if opened_prompt and opened_prompt.user_prompt_type == "prompt":
            add_result(
                results,
                "browsingContext.userPromptOpened prompt",
                "成功",
                opened_prompt.message or "",
            )
        else:
            add_result(
                results,
                "browsingContext.userPromptOpened prompt",
                "失败",
                "未观察到 prompt 打开事件",
            )

        closed_prompt = page.events.wait("browsingContext.userPromptClosed", timeout=3)
        if closed_prompt and closed_prompt.accepted is True:
            add_result(
                results,
                "browsingContext.userPromptClosed prompt",
                "成功",
                f"accepted={closed_prompt.accepted}",
            )
        else:
            add_result(
                results,
                "browsingContext.userPromptClosed prompt",
                "跳过",
                "当前环境下 prompt 自动注入文本后未稳定观察到 userPromptClosed",
            )

        page.clear_prompt_handler()
        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.clear_prompt_handler()
        except Exception:
            pass
        try:
            page.events.stop()
        except Exception:
            pass
        if temp_tab:
            try:
                page.contexts.close(temp_tab)
            except Exception:
                pass
        if temp_window:
            try:
                page.contexts.close(temp_window)
            except Exception:
                pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

网络事件:请求开始、响应开始、响应完成、错误与认证

examples/31_network_events.py
GitHub 仓库 已同步源码 查看 GitHub 源码
network strict
一句话定位

是 network-events 页最直接的主示例。

涉及 API
page.events.startpage.events.waitpage.intercept.startpage.intercept.waitcontinue_with_auth
展示的不同用法
  • 观察 beforeRequestSent、responseStarted、responseCompleted
  • 监听 fetchError
  • 处理 authRequired 并补充用户名密码
适用场景

适合 network-events 与 auth 页面。

相关文档页面
推荐阅读
advanced-network-18collector-28
完整代码

来源:app/content/ruyipage_examples/31_network_events.py · 仓库文件:31_network_events.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 网络事件:请求开始、响应开始、响应完成、错误与认证
# 说明: 是 network-events 页最直接的主示例。
# 你会学到:
#   1. 观察 beforeRequestSent、responseStarted、responseCompleted
#   2. 监听 fetchError
#   3. 处理 authRequired 并补充用户名密码
# 相关 API: page.events.start, page.events.wait, page.intercept.start, page.intercept.wait, continue_with_auth
# =============================

# -*- coding: utf-8 -*-
"""示例31: Network Events(严格结果版)"""

import io
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 FirefoxPage, InterceptedRequest
from ruyipage._functions.tools import find_free_port

from test_server import TestServer


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("测试 31: Network Events")
    print("=" * 70)

    page = FirefoxPage()
    server = None
    results: List[Dict[str, str]] = []

    try:
        server = TestServer(port=find_free_port(9330, 9430)).start()
        base_url = server.get_url("")[:-1]

        page.get("about:blank")
        add_result(
            results, "页面加载", "成功", f"about:blank 已加载,服务地址: {base_url}"
        )

        page.events.start(
            [
                "network.beforeRequestSent",
                "network.responseStarted",
                "network.responseCompleted",
                "network.fetchError",
                "network.authRequired",
            ],
            contexts=[page.tab_id],
        )

        # 1. 普通请求事件链
        page.events.clear()
        page.run_js(
            f"fetch('{base_url}/api/data').catch(()=>null); return true;",
            as_expr=False,
        )

        before_event = page.events.wait("network.beforeRequestSent", timeout=5)
        started_event = page.events.wait("network.responseStarted", timeout=5)
        completed_event = page.events.wait("network.responseCompleted", timeout=5)

        if before_event and str((before_event.request or {}).get("url", "")).endswith(
            "/api/data"
        ):
            add_result(
                results,
                "network.beforeRequestSent",
                "成功",
                (before_event.request or {}).get("url", ""),
            )
        else:
            add_result(
                results,
                "network.beforeRequestSent",
                "失败",
                "未观察到 /api/data 请求开始事件",
            )

        if (
            started_event
            and int((started_event.response or {}).get("status", 0)) == 200
        ):
            add_result(
                results,
                "network.responseStarted",
                "成功",
                f"status={(started_event.response or {}).get('status')}",
            )
        else:
            add_result(
                results, "network.responseStarted", "失败", "未观察到 200 响应开始事件"
            )

        if (
            completed_event
            and int((completed_event.response or {}).get("status", 0)) == 200
        ):
            add_result(
                results,
                "network.responseCompleted",
                "成功",
                f"status={(completed_event.response or {}).get('status')}",
            )
        else:
            add_result(
                results,
                "network.responseCompleted",
                "失败",
                "未观察到 200 响应完成事件",
            )

        # 2. fetchError
        page.events.clear()
        page.run_js(
            "fetch('http://127.0.0.1:9/').catch(()=>null); return true;",
            as_expr=False,
        )
        error_event = page.events.wait("network.fetchError", timeout=5)
        if error_event:
            add_result(
                results, "network.fetchError", "成功", error_event.error_text or ""
            )
        else:
            add_result(
                results, "network.fetchError", "跳过", "当前环境未稳定观察到 fetchError"
            )

        # 3. authRequired
        page.events.clear()
        page.intercept.start(
            url_patterns=[{"type": "string", "pattern": base_url + "/api/auth"}],
            phases=["authRequired"],
        )
        try:
            page.get(base_url + "/api/auth")
        except Exception:
            pass

        auth_req: InterceptedRequest | None = page.intercept.wait(timeout=5)
        auth_event = page.events.wait("network.authRequired", timeout=5)

        if auth_event and auth_req:
            add_result(results, "network.authRequired", "成功", auth_req.url)
            auth_req.continue_with_auth(
                action="provideCredentials",
                username="user",
                password="pass",
            )

            auth_done = page.events.wait("network.responseCompleted", timeout=5)
            if auth_done and int((auth_done.response or {}).get("status", 0)) == 200:
                add_result(
                    results,
                    "network.authRequired credentials",
                    "成功",
                    "提供凭证后认证通过",
                )
            else:
                add_result(
                    results,
                    "network.authRequired credentials",
                    "失败",
                    "提供凭证后未观察到 200 完成事件",
                )
        else:
            add_result(
                results,
                "network.authRequired",
                "跳过",
                "当前环境未稳定观察到 authRequired",
            )
            add_result(
                results,
                "network.authRequired credentials",
                "跳过",
                "未观察到 authRequired,跳过凭证验证",
            )

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.intercept.stop()
        except Exception:
            pass
        try:
            page.events.stop()
        except Exception:
            pass
        try:
            if server is not None:
                server.stop()
        except Exception:
            pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

Script 事件:realm 创建销毁与 preload 消息

examples/32_script_events.py
GitHub 仓库 已同步源码 查看 GitHub 源码
debug strict
一句话定位

用于说明 realms 和 preload script 如何和事件订阅配合。

涉及 API
page.realms.startpage.realms.listpage.events.startpage.events.waitpage.add_preload_scriptpage.remove_preload_script
展示的不同用法
  • 观察 realm 创建与销毁
  • 订阅 script.message
  • 通过 preload script 发送事件消息
适用场景

适合 script 和 preload-scripts 页面。

推荐阅读
javascript-07script-input-29
完整代码

来源:app/content/ruyipage_examples/32_script_events.py · 仓库文件:32_script_events.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: Script 事件:realm 创建销毁与 preload 消息
# 说明: 用于说明 realms 和 preload script 如何和事件订阅配合。
# 你会学到:
#   1. 观察 realm 创建与销毁
#   2. 订阅 script.message
#   3. 通过 preload script 发送事件消息
# 相关 API: page.realms.start, page.realms.list, page.events.start, page.events.wait, page.add_preload_script, page.remove_preload_script
# =============================

# -*- coding: utf-8 -*-
"""示例32: Script Events(严格结果版)"""

import io
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 FirefoxPage, PreloadScript


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("测试 32: Script Events")
    print("=" * 70)

    page = FirefoxPage()
    results: List[Dict[str, str]] = []
    temp_tab = None
    preload: PreloadScript | None = None

    try:
        page.get("https://www.example.com")
        add_result(results, "页面加载", "成功", "example.com 已加载")

        # 1. realmCreated / realmDestroyed
        page.realms.start()
        initial_realms = page.realms.list()
        add_result(
            results,
            "script.getRealms baseline",
            "成功",
            f"初始 realm 数量: {len(initial_realms)}",
        )

        temp_tab = page.contexts.create_tab()
        page.wait(1)
        created_realms = page.realms.list()
        if len(created_realms) > len(initial_realms):
            add_result(
                results,
                "script.realmCreated",
                "成功",
                f"realm 数量: {len(initial_realms)} -> {len(created_realms)}",
            )
        else:
            add_result(
                results, "script.realmCreated", "跳过", "当前环境未稳定观察到新增 realm"
            )

        if temp_tab:
            page.contexts.close(temp_tab)
            temp_tab = None
            page.wait(1)

        destroyed_realms = page.realms.list()
        if len(destroyed_realms) <= len(created_realms):
            add_result(
                results,
                "script.realmDestroyed",
                "成功",
                f"关闭后 realm 数量: {len(destroyed_realms)}",
            )
        else:
            add_result(
                results,
                "script.realmDestroyed",
                "跳过",
                "当前环境未稳定观察到 realm 销毁",
            )

        page.realms.stop()

        # 2. script.message
        if page.events.start(["script.message"], contexts=[page.tab_id]):
            preload = page.add_preload_script(
                """
                () => {
                    const ch = new BroadcastChannel('ruyi-script-message');
                    ch.postMessage({ type: 'preload', value: 'hello' });
                    ch.close();
                }
                """
            )

            page.get("https://www.example.com/?script-message=1")
            message_event = page.events.wait("script.message", timeout=3)

            if message_event:
                add_result(
                    results,
                    "script.message",
                    "成功",
                    f"channel={message_event.channel} data={message_event.data}",
                )
            else:
                add_result(
                    results,
                    "script.message",
                    "跳过",
                    "当前环境未稳定观察到 script.message",
                )
        else:
            add_result(
                results, "script.message", "不支持", "未能订阅 script.message 事件"
            )

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        if preload is not None:
            try:
                page.remove_preload_script(preload)
            except Exception:
                pass
        try:
            page.realms.stop()
        except Exception:
            pass
        try:
            page.events.stop()
        except Exception:
            pass
        if temp_tab:
            try:
                page.contexts.close(temp_tab)
            except Exception:
                pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

日志与输入事件:log.entryAdded 与 fileDialogOpened

examples/33_log_input_events.py
GitHub 仓库 已同步源码 查看 GitHub 源码
debug strict
一句话定位

把日志和输入事件统一纳入事件系统理解。

涉及 API
page.console.startpage.console.waitpage.events.startpage.events.waitclick(by_js=True)
展示的不同用法
  • 输出并监听多级别 console 日志
  • 读取 error / warn / table 内容
  • 订阅 fileDialogOpened 事件
  • 观察单文件和多文件输入弹窗打开链路
适用场景

适合 console、events 和 forms 页面。

相关文档页面
推荐阅读
console-12downloads-23
完整代码

来源:app/content/ruyipage_examples/33_log_input_events.py · 仓库文件:33_log_input_events.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 日志与输入事件:log.entryAdded 与 fileDialogOpened
# 说明: 把日志和输入事件统一纳入事件系统理解。
# 你会学到:
#   1. 输出并监听多级别 console 日志
#   2. 读取 error / warn / table 内容
#   3. 订阅 fileDialogOpened 事件
#   4. 观察单文件和多文件输入弹窗打开链路
# 相关 API: page.console.start, page.console.wait, page.events.start, page.events.wait, click(by_js=True)
# =============================

# -*- coding: utf-8 -*-
"""示例33: Log + Input Events(严格结果版)"""

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 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("测试 33: Log + Input Events")
    print("=" * 70)

    page = FirefoxPage()
    results: List[Dict[str, str]] = []
    test_file = os.path.join(os.path.dirname(__file__), "test_file_dialog.html")

    try:
        # 1. log.entryAdded
        page.get("https://www.example.com")
        add_result(results, "页面加载", "成功", "example.com 已加载")

        page.console.start()
        page.console.clear()

        page.run_js("console.log('This is a log message')", as_expr=False)
        page.run_js("console.info('This is an info message')", as_expr=False)
        page.run_js("console.warn('This is a warning message')", as_expr=False)
        page.run_js("console.error('This is an error message')", as_expr=False)
        page.run_js("console.debug('This is a debug message')", as_expr=False)
        page.run_js(
            "console.table([{name: 'Alice', age: 18}, {name: 'Bob', age: 20}])",
            as_expr=False,
        )

        log_entry = page.console.wait(timeout=3)
        entries = page.console.entries
        error_entry = next((e for e in entries if e.level == "error"), None)
        warn_entry = next((e for e in entries if e.level == "warn"), None)
        table_entry = next(
            (e for e in entries if "Alice" in (e.text or "") or "Bob" in (e.text or "")),
            None,
        )

        if log_entry:
            add_result(
                results,
                "log.entryAdded first",
                "成功",
                f"level={log_entry.level} text={log_entry.text}",
            )
        else:
            add_result(results, "log.entryAdded first", "失败", "未观察到首条日志事件")

        if error_entry:
            add_result(results, "log.entryAdded error", "成功", error_entry.text or "")
        else:
            add_result(
                results, "log.entryAdded error", "失败", "未观察到 error 日志事件"
            )

        if warn_entry:
            add_result(results, "log.entryAdded warn", "成功", warn_entry.text or "")
        else:
            add_result(results, "log.entryAdded warn", "失败", "未观察到 warn 日志事件")

        if table_entry:
            add_result(results, "log.entryAdded table", "成功", (table_entry.text or "")[:120])
        else:
            add_result(results, "log.entryAdded table", "跳过", "当前 console.table 输出未稳定映射到 text")

        add_result(
            results,
            "log.entryAdded total",
            "成功" if len(entries) >= 6 else "失败",
            f"日志数量: {len(entries)}",
        )
        page.console.stop()


        # 2. input.fileDialogOpened
        html_content = """
        <!DOCTYPE html>
        <html>
        <head><meta charset='utf-8'><title>File Dialog Test</title></head>
        <body>
            <input type='file' id='single-file' />
            <input type='file' id='multiple-files' multiple />
            <button id='trigger-single' onclick="document.getElementById('single-file').click()">Open Single</button>
            <button id='trigger-multiple' onclick="document.getElementById('multiple-files').click()">Open Multiple</button>
        </body>
        </html>
        """
        with open(test_file, "w", encoding="utf-8") as f:
            f.write(html_content)

        page.get(f"file:///{test_file.replace(os.sep, '/')}")
        add_result(results, "文件测试页加载", "成功", "本地 file dialog 页面已加载")

        if page.events.start(["input.fileDialogOpened"], contexts=[page.tab_id]):
            page.events.clear()
            page.ele("#trigger-single").click(by_js=True)
            single_event = page.events.wait("input.fileDialogOpened", timeout=3)
            if single_event and single_event.multiple is False:
                add_result(
                    results,
                    "input.fileDialogOpened single",
                    "成功",
                    f"multiple={single_event.multiple}",
                )
            else:
                add_result(
                    results,
                    "input.fileDialogOpened single",
                    "跳过",
                    "当前环境未稳定观察到单文件对话框事件",
                )

            page.events.clear()
            page.ele("#trigger-multiple").click(by_js=True)
            multi_event = page.events.wait("input.fileDialogOpened", timeout=3)
            if multi_event and multi_event.multiple is True:
                add_result(
                    results,
                    "input.fileDialogOpened multiple",
                    "成功",
                    f"multiple={multi_event.multiple}",
                )
            else:
                add_result(
                    results,
                    "input.fileDialogOpened multiple",
                    "跳过",
                    "当前环境未稳定观察到多文件对话框事件",
                )
        else:
            add_result(
                results,
                "input.fileDialogOpened",
                "不支持",
                "未能订阅 input.fileDialogOpened 事件",
            )

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.console.stop()
        except Exception:
            pass
        try:
            page.events.stop()
        except Exception:
            pass
        try:
            if os.path.exists(test_file):
                os.remove(test_file)
        except Exception:
            pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

剩余命令覆盖:截图、历史、继续响应、mock、fail、认证、动作释放

examples/34_remaining_commands.py
GitHub 仓库 已同步源码 查看 GitHub 源码
strict strict
一句话定位

把一些散落但重要的能力集中演示,适合做协议能力查漏补缺。

涉及 API
page.screenshotpage.backpage.forwardpage.intercept.start_responsesreq.continue_responsereq.mockreq.failreq.continue_with_authpage.actions.release
展示的不同用法
  • 截图与保存文件
  • 验证历史遍历
  • 继续响应和 mock 响应
  • 请求失败与认证补充
  • 释放动作链状态
适用场景

适合 strict 与 testing 章节。

相关文档页面
推荐阅读
network-events-31advanced-network-18
完整代码

来源:app/content/ruyipage_examples/34_remaining_commands.py · 仓库文件:34_remaining_commands.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 剩余命令覆盖:截图、历史、继续响应、mock、fail、认证、动作释放
# 说明: 把一些散落但重要的能力集中演示,适合做协议能力查漏补缺。
# 你会学到:
#   1. 截图与保存文件
#   2. 验证历史遍历
#   3. 继续响应和 mock 响应
#   4. 请求失败与认证补充
#   5. 释放动作链状态
# 相关 API: page.screenshot, page.back, page.forward, page.intercept.start_responses, req.continue_response, req.mock, req.fail, req.continue_with_auth
# =============================

# -*- coding: utf-8 -*-
"""示例34: Remaining Commands(严格结果版)"""

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 FirefoxPage, InterceptedRequest
from ruyipage._functions.tools import find_free_port

from test_server import TestServer


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("测试 34: Remaining Commands")
    print("=" * 70)

    page = FirefoxPage()
    server = None
    results: List[Dict[str, str]] = []
    screenshot_path = os.path.join(os.path.dirname(__file__), "test_screenshot.png")

    try:
        server = TestServer(port=find_free_port(9430, 9530)).start()
        base_url = server.get_url("")[:-1]

        # 1. captureScreenshot
        page.get(base_url + "/nav/basic")
        add_result(results, "页面加载", "成功", f"服务地址: {base_url}")

        data_bytes = page.screenshot(as_bytes=True)
        if data_bytes and len(data_bytes) > 100:
            add_result(
                results,
                "browsingContext.captureScreenshot viewport",
                "成功",
                f"截图字节数: {len(data_bytes)}",
            )
        else:
            add_result(
                results,
                "browsingContext.captureScreenshot viewport",
                "失败",
                "截图数据过短",
            )

        saved_path = page.screenshot(path=screenshot_path)
        if saved_path and os.path.exists(saved_path):
            add_result(results, "screenshot save file", "成功", saved_path)
        else:
            add_result(results, "screenshot save file", "失败", "未保存截图文件")

        # 2. traverseHistory
        page.get(base_url + "/nav/basic?a=1")
        page.get(base_url + "/nav/basic?a=2")
        page.get(base_url + "/nav/basic?a=3")
        page.back()
        back_url = page.url
        page.back()
        back2_url = page.url
        page.forward()
        forward_url = page.url

        if (
            back_url.endswith("?a=2")
            and back2_url.endswith("?a=1")
            and forward_url.endswith("?a=2")
        ):
            add_result(
                results,
                "browsingContext.traverseHistory",
                "成功",
                f"back={back_url}, back2={back2_url}, forward={forward_url}",
            )
        else:
            add_result(
                results,
                "browsingContext.traverseHistory",
                "失败",
                f"back={back_url}, back2={back2_url}, forward={forward_url}",
            )

        # 3. network.continueResponse
        page.intercept.start_responses(
            url_patterns=[{"type": "string", "pattern": base_url + "/api/data"}]
        )
        page.run_js(
            f"fetch('{base_url}/api/data').catch(()=>null); return true;", as_expr=False
        )
        resp_req: InterceptedRequest | None = page.intercept.wait(timeout=5)
        if resp_req:
            resp_req.continue_response()
            add_result(results, "network.continueResponse", "成功", resp_req.url)
        else:
            add_result(
                results,
                "network.continueResponse",
                "跳过",
                "未稳定捕获到 responseStarted 拦截",
            )
        page.intercept.stop()

        # 4. network.provideResponse
        page.intercept.start_requests(
            url_patterns=[{"type": "string", "pattern": base_url + "/api/mock-source"}]
        )
        page.run_js(
            f"fetch('{base_url}/api/mock-source').catch(()=>null); return true;",
            as_expr=False,
        )
        mock_req: InterceptedRequest | None = page.intercept.wait(timeout=5)
        if mock_req:
            mock_req.mock(body='{"status":"mocked"}', status_code=200)
            add_result(results, "network.provideResponse", "成功", mock_req.url)
        else:
            add_result(
                results,
                "network.provideResponse",
                "跳过",
                "未稳定捕获到 beforeRequestSent 拦截",
            )
        page.intercept.stop()

        # 5. network.failRequest
        page.intercept.start_requests(
            url_patterns=[{"type": "string", "pattern": base_url + "/api/slow"}]
        )
        page.run_js(
            f"fetch('{base_url}/api/slow').catch(()=>null); return true;", as_expr=False
        )
        fail_req: InterceptedRequest | None = page.intercept.wait(timeout=5)
        if fail_req:
            fail_req.fail()
            add_result(results, "network.failRequest", "成功", fail_req.url)
        else:
            add_result(
                results,
                "network.failRequest",
                "跳过",
                "未稳定捕获到 beforeRequestSent 拦截",
            )
        page.intercept.stop()

        # 6. network.continueWithAuth
        page.events.start(["network.authRequired"], contexts=[page.tab_id])
        page.intercept.start(
            url_patterns=[{"type": "string", "pattern": base_url + "/api/auth"}],
            phases=["authRequired"],
        )
        try:
            page.get(base_url + "/api/auth")
        except Exception:
            pass
        auth_req: InterceptedRequest | None = page.intercept.wait(timeout=5)
        auth_event = page.events.wait("network.authRequired", timeout=5)
        if auth_req and auth_event:
            auth_req.continue_with_auth(
                action="provideCredentials",
                username="user",
                password="pass",
            )
            add_result(results, "network.continueWithAuth", "成功", auth_req.url)
        else:
            add_result(
                results,
                "network.continueWithAuth",
                "跳过",
                "当前环境未稳定观察到 authRequired",
            )
        page.intercept.stop()
        page.events.stop()

        # 7. input.performActions / releaseActions
        try:
            page.quit()
        except Exception:
            pass
        page = FirefoxPage()

        action_page = """
        <!DOCTYPE html>
        <html>
        <head><meta charset='utf-8'><title>Actions Test</title></head>
        <body>
            <input id='input-box' autofocus />
            <button id='btn' onclick="window.btnClicked = true">Click Me</button>
            <script>
                window.btnClicked = false;
                window.lastKey = '';
                document.getElementById('input-box').addEventListener('keydown', function(e) {
                    window.lastKey = e.key;
                });
            </script>
        </body>
        </html>
        """
        page.get("data:text/html;charset=utf-8," + action_page)
        page.actions.move_to(page.ele("#input-box")).click().press("a").perform()
        last_key = page.run_js("return window.lastKey")
        page.actions.move_to(page.ele("#btn")).click().perform()
        btn_clicked = page.run_js("return window.btnClicked")
        page.actions.release()

        if last_key == "a":
            add_result(
                results, "input.performActions keyboard", "成功", f"lastKey={last_key}"
            )
        else:
            add_result(
                results, "input.performActions keyboard", "失败", f"lastKey={last_key}"
            )

        if btn_clicked is True:
            add_result(
                results, "input.performActions pointer", "成功", "按钮点击事件已触发"
            )
        else:
            add_result(
                results, "input.performActions pointer", "失败", "按钮点击事件未触发"
            )

        add_result(results, "input.releaseActions", "成功", "动作状态已释放")

        print_results(results)

    except Exception as e:
        add_result(results, "示例执行", "失败", str(e)[:120])
        print_results(results)
        raise
    finally:
        try:
            page.intercept.stop()
        except Exception:
            pass
        try:
            page.events.stop()
        except Exception:
            pass
        try:
            if os.path.exists(screenshot_path):
                os.remove(screenshot_path)
        except Exception:
            pass
        try:
            if server is not None:
                server.stop()
        except Exception:
            pass
        try:
            page.quit()
        except Exception:
            pass


if __name__ == "__main__":
    main()

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

examples/35_native_bidi_drag.py
GitHub 仓库 已同步源码 查看 GitHub 源码
actions strict
一句话定位

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

涉及 API
page.actions.dragpage.actions.releasepage.run_jspage.ele
展示的不同用法
  • 只用高层 API 实现拖拽
  • 验证 drop 是否命中目标
  • 统计 trusted move 次数和鼠标事件状态
适用场景

适合 drag-drop 与 is-trusted 页面。

相关文档页面
推荐阅读
advanced-input-20actions-chain-05
完整代码

来源: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()

原生 Select 选择:只测 native_only 模式与事件诊断

examples/36_native_bidi_select.py
GitHub 仓库 已同步源码 查看 GitHub 源码
elements strict
一句话定位

专门帮助排查 select 的焦点、键盘与 trusted change 路径问题。

涉及 API
select.by_valueselect.selected_optionclick_selfpage.actions.key_downpage.actions.key_uppage.run_js
展示的不同用法
  • 检查 select 初始状态
  • 聚焦后通过键盘切换选项
  • 强制只走 native_only 模式
  • 读取 changeCount 和 trustedChange 诊断信息
适用场景

适合 forms、actions 和 strict 示例讲解。

相关文档页面
推荐阅读
elements-03advanced-input-20
完整代码

来源:app/content/ruyipage_examples/36_native_bidi_select.py · 仓库文件:36_native_bidi_select.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 原生 Select 选择:只测 native_only 模式与事件诊断
# 说明: 专门帮助排查 select 的焦点、键盘与 trusted change 路径问题。
# 你会学到:
#   1. 检查 select 初始状态
#   2. 聚焦后通过键盘切换选项
#   3. 强制只走 native_only 模式
#   4. 读取 changeCount 和 trustedChange 诊断信息
# 相关 API: select.by_value, select.selected_option, click_self, page.actions.key_down, page.actions.key_up, page.run_js
# =============================

# -*- coding: utf-8 -*-
"""
示例36: 原生 BiDi Select 检测(仅原生)

目标:
1) 只测试 native_only(不允许 JS 保底)
2) 输出分步诊断信息,帮助定位焦点/键盘路径问题
"""

import io
import os
import sys


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 FirefoxOptions, FirefoxPage, Keys


def _state(page, select):
    s = page.run_js("return window.nativeBidiSelectState")
    return {
        "value": select.value,
        "selected_option": select.select.selected_option,
        "focused": s.get("focused"),
        "trusted_change": s.get("trustedChange"),
        "change_count": s.get("changeCount"),
        "events": s.get("events", []),
    }


def test_native_bidi_select():
    print("=" * 60)
    print("测试36: 原生 BiDi Select(仅原生)")
    print("=" * 60)

    opts = FirefoxOptions()
    opts.headless(False)
    page = FirefoxPage(opts)

    try:
        test_page = os.path.join(
            os.path.dirname(__file__), "test_pages", "native_bidi_select_test.html"
        )
        test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
        page.get(test_url)
        page.wait(1)

        select = page.ele("#single-select")

        print("\n1. 初始状态:")
        st = _state(page, select)
        print(f"   value: {st['value']}")
        print(f"   selected_option: {st['selected_option']}")
        print(f"   focused: {st['focused']}")

        print("\n2. 手动焦点探测(原生 click + 一步键盘):")
        select.click_self()
        page.wait(0.1)
        page.actions.key_down(Keys.DOWN).key_up(Keys.DOWN).perform()
        page.wait(0.1)
        st_probe = _state(page, select)
        print(f"   probe value: {st_probe['value']}")
        print(f"   probe focused: {st_probe['focused']}")
        print(f"   probe changeCount: {st_probe['change_count']}")

        print("\n3. 使用 selector native_only 选择 opt2:")
        ok = select.select.by_value("opt2", mode="native_only")
        page.wait(0.3)
        st = _state(page, select)
        print(f"   native_only结果: {ok}")
        print(f"   value: {st['value']}")
        print(f"   selected_option: {st['selected_option']}")
        print(f"   focused: {st['focused']}")
        print(f"   trustedChange: {st['trusted_change']}")
        print(f"   changeCount: {st['change_count']}")

        print("\n4. 页面事件日志:")
        for line in st["events"]:
            print(f"   {line}")

        if not ok or select.value != "opt2":
            raise RuntimeError("native_only 未成功切换到 opt2")

        print("\n" + "=" * 60)
        print("✓ 原生 BiDi Select 测试通过!")
        print("=" * 60)

    except Exception as e:
        print(f"\n✗ 测试失败: {e}")
        import traceback

        traceback.print_exc()
    finally:
        page.wait(1)
        page.quit()


if __name__ == "__main__":
    test_native_bidi_select()

Debug Trace 与 Failure Snapshot 诊断演示

examples/47_trace_and_diagnostics.py
GitHub 仓库 已同步源码 查看 GitHub 源码
debug advanced
一句话定位

完整演示 ruyipage 的调试诊断与观测系统:Trace 时间线记录、FailureSnapshot 失败自动快照、异常的 .diagnostics 属性和敏感字段自动脱敏。

涉及 API
launch(trace=True)page.trace.enabledpage.trace.latestpage.trace.recent_requestspage.trace.summarypage.trace.dump_jsonpage.trace.clearFailureSnapshotTraceEntrye.diagnosticsFirefoxOptions.enable_traceFirefoxOptions.enable_failure_snapshotFirefoxOptions.set_snapshot_dirSettings.trace_enabledSettings.failure_snapshot_enabled
展示的不同用法
  • 三种方式启用 Trace:launch() 参数 / FirefoxOptions / Settings 全局
  • Trace 时间线 API:latest()、summary()、dump_json()、recent_requests()
  • Trace 缓冲区管理:clear()、enabled、repr
  • Failure Snapshot 失败自动诊断:截图、DOM、URL、trace、网络请求
  • FailureSnapshot 数据 API:summary()、to_json()、to_dict()
  • 异常的 .diagnostics 属性:从 except 块直接访问诊断信息
  • 敏感字段自动脱敏:password / cookie / authorization 等
  • TraceEntry 数据结构:category、event、data、elapsed_ms、to_dict()
适用场景

适合调试排错、生产环境诊断和自动化测试失败分析。

相关文档页面
推荐阅读
console-12log-input-33javascript-07
完整代码

来源:app/content/ruyipage_examples/47_trace_and_diagnostics.py · 仓库文件:47_trace_and_diagnostics.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: Debug Trace 与 Failure Snapshot 诊断演示
# 说明: 完整演示 ruyipage 的调试诊断与观测系统:Trace 时间线记录、FailureSnapshot 失败自动快照、异常的 .diagnostics 属性和敏感字段自动脱敏。
# 你会学到:
#   1. 三种方式启用 Trace:launch() 参数 / FirefoxOptions / Settings 全局
#   2. Trace 时间线 API:latest()、summary()、dump_json()、recent_requests()
#   3. Trace 缓冲区管理:clear()、enabled、repr
#   4. Failure Snapshot 失败自动诊断:截图、DOM、URL、trace、网络请求
#   5. FailureSnapshot 数据 API:summary()、to_json()、to_dict()
# 相关 API: launch(trace=True), page.trace.enabled, page.trace.latest, page.trace.recent_requests, page.trace.summary, page.trace.dump_json, page.trace.clear, FailureSnapshot
# =============================

# -*- coding: utf-8 -*-
"""Example 47: Debug Trace 与 Failure Snapshot 诊断演示

专门演示 ruyipage 的调试诊断与观测功能:

1. 三种启用 Trace 的方式(launch / FirefoxOptions / Settings)
2. Trace 时间线 API — summary(), dump_json(), latest(), recent_requests()
3. Trace 缓冲区管理 — clear(), enabled, repr
4. Failure Snapshot — 操作失败时自动抓取截图/DOM/URL/trace/网络
5. FailureSnapshot 数据 API — summary(), to_json(), to_dict()
6. 异常的 .diagnostics 属性 — 从 except 块直接访问诊断信息
7. 敏感字段自动脱敏 — password / cookie / authorization 等

注意:Trace 功能默认关闭,不影响正常使用性能。
"""

import io
import json
import sys
import time

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 (
    launch,
    FirefoxOptions,
    FirefoxPage,
    Settings,
    TraceEntry,
    FailureSnapshot,
)
from ruyipage.errors import ElementNotFoundError, JavaScriptError, WaitTimeoutError

from test_server import TestServer


def sep(title):
    print("\n" + "=" * 60)
    print(f"  {title}")
    print("=" * 60)


# =====================================================================
# Part 1: 三种启用方式
# =====================================================================

def demo_enable_methods():
    sep("Part 1: 三种启用 Trace 的方式")

    # --- 方式 A: launch() 参数(最简单) ---
    print("\n--- 方式 A: launch(trace=True) ---")
    print("  page = launch(trace=True)")
    print("  # 即可开始记录所有 BiDi 命令和事件")

    # --- 方式 B: FirefoxOptions 链式调用 ---
    print("\n--- 方式 B: FirefoxOptions ---")
    print("  opts = FirefoxOptions()")
    print("  opts.enable_trace(True)           # 启用 trace")
    print("  opts.enable_failure_snapshot()     # 启用失败快照")
    print("  opts.set_snapshot_dir('./diag')    # 快照保存目录")
    print("  page = FirefoxPage(opts)")

    # --- 方式 C: Settings 全局开关 ---
    print("\n--- 方式 C: Settings 全局开关 ---")
    print("  Settings.trace_enabled = True      # 全局开启")
    print("  Settings.trace_max_entries = 2000  # 调整缓冲区大小")
    print("  Settings.failure_snapshot_enabled = True")


# =====================================================================
# Part 2: Trace 时间线 API 全演示
# =====================================================================

def demo_trace_api(page, server):
    sep("Part 2: Trace 时间线 API")
    base = server.get_url

    # 2.1 做一些操作,让 trace 缓冲区积累数据
    print("\n[2.1] 执行操作,积累 trace 数据...")
    page.get(base("/nav/basic"))
    page.wait(0.5)
    page.get(base("/nav/history"))
    page.wait(0.5)
    page.run_js("document.title")
    page.ele("tag:h1")

    # 2.2 trace.enabled — 查看当前是否启用
    print(f"\n[2.2] page.trace.enabled = {page.trace.enabled}")

    # 2.3 trace.latest(n) — 获取最近 N 条 TraceEntry
    print("\n[2.3] page.trace.latest(5) — 最近 5 条记录:")
    entries = page.trace.latest(5)
    for e in entries:
        print(f"  {e}")
    print(f"  (共 {len(entries)} 条)")

    # 2.4 TraceEntry 属性
    if entries:
        e = entries[-1]
        print(f"\n[2.4] TraceEntry 详细属性:")
        print(f"  category   = {e.category}")
        print(f"  event      = {e.event}")
        print(f"  status     = {e.status}")
        print(f"  elapsed_ms = {e.elapsed_ms}")
        print(f"  context_id = {e.context_id}")
        print(f"  data       = {e.data}")

        # 2.4.1 to_dict()
        d = e.to_dict()
        print(f"\n  TraceEntry.to_dict() 输出:")
        print(f"    {json.dumps(d, ensure_ascii=False)}")

    # 2.5 trace.summary() — 人类可读摘要
    print(f"\n[2.5] page.trace.summary(10):\n")
    print(page.trace.summary(10))

    # 2.6 trace.dump_json() — 全量 JSON 导出
    print(f"\n[2.6] page.trace.dump_json() — 前 500 字符:")
    j = page.trace.dump_json()
    print(j[:500])
    if len(j) > 500:
        print(f"  ...(总共 {len(j)} 字符)")

    # 2.7 repr
    print(f"\n[2.7] repr(page.trace) = {repr(page.trace)}")


# =====================================================================
# Part 3: 网络请求记录
# =====================================================================

def demo_network_trace(page, server):
    sep("Part 3: 网络请求记录 (trace.recent_requests)")
    base = server.get_url

    # 先订阅网络事件,trace 的 recv_loop 被动钩子才能捕获
    page.listen.start()

    page.get(base("/nav/basic"))
    page.wait(0.5)

    # 用 JS 发起一些请求
    page.run_js(f"""
        fetch('{base("/api/data")}');
        fetch('{base("/api/headers")}');
    """)
    page.wait(1)

    # 查看网络请求记录
    reqs = page.trace.recent_requests(10)
    print(f"\n最近 {len(reqs)} 条网络请求:")
    for r in reqs:
        print(f"  [{r.get('method', '?'):>4}] {r.get('status', '?'):>3}  "
              f"{r.get('url', '?')[:80]}")

    page.listen.stop()


# =====================================================================
# Part 4: Trace 缓冲区管理
# =====================================================================

def demo_trace_management(page):
    sep("Part 4: 缓冲区管理")

    count_before = len(page.trace.latest(9999))
    print(f"\n[4.1] 清空前: {count_before} 条记录")

    page.trace.clear()

    count_after = len(page.trace.latest(9999))
    print(f"[4.2] 清空后: {count_after} 条记录")

    # 执行新操作
    page.run_js("1 + 1")
    page.run_js("document.title")

    count_new = len(page.trace.latest(9999))
    print(f"[4.3] 新操作后: {count_new} 条记录")

    # summary 方法在空缓冲区时的行为
    page.trace.clear()
    print(f"\n[4.4] 空缓冲区 summary: '{page.trace.summary()}'")


# =====================================================================
# Part 5: Failure Snapshot — 失败自动诊断
# =====================================================================

def demo_failure_snapshot(page, server):
    sep("Part 5: Failure Snapshot — 失败自动诊断")
    base = server.get_url

    page.get(base("/nav/basic"))
    page.wait(0.5)

    # 5.1 ElementNotFoundError — 查找不存在的元素
    # ele() 默认不抛异常(返回 NoneElement),需临时开启 raise 模式
    print("\n[5.1] 触发 ElementNotFoundError:")
    print("  Settings.raise_when_ele_not_found = True")
    print("  page.ele('#this-element-does-not-exist', timeout=2)")
    orig_raise = Settings.raise_when_ele_not_found
    Settings.raise_when_ele_not_found = True
    try:
        page.ele("#this-element-does-not-exist", timeout=2)
    except ElementNotFoundError as e:
        print(f"  捕获异常: {type(e).__name__}: {e}")

        if e.diagnostics:
            snap = e.diagnostics
            print(f"\n  异常的 .diagnostics 属性 (FailureSnapshot):")
            print(f"    error_type     = {snap.error_type}")
            print(f"    error_message  = {snap.error_message[:80]}")
            print(f"    url            = {snap.url}")
            print(f"    context_id     = {snap.context_id}")
            print(f"    screenshot     = {snap.screenshot_path or '(未保存到文件)'}")
            print(f"    dom_path       = {snap.dom_path or '(未保存到文件)'}")
            print(f"    trace_entries  = {len(snap.trace_entries)} 条")
            print(f"    recent_reqs    = {len(snap.recent_requests)} 条")
            print(f"    capture_errors = {snap.capture_errors}")

            # 5.1.1 summary()
            print(f"\n  FailureSnapshot.summary():\n")
            print(snap.summary())

            # 5.1.2 to_json()
            print(f"\n  FailureSnapshot.to_json() — 前 400 字符:")
            j = snap.to_json()
            print(j[:400])
            if len(j) > 400:
                print(f"  ...(总共 {len(j)} 字符)")

            # 5.1.3 to_dict()
            d = snap.to_dict()
            print(f"\n  to_dict() 的 keys: {list(d.keys())}")

        else:
            print("  (diagnostics 为 None — 未启用 failure_snapshot)")
    finally:
        Settings.raise_when_ele_not_found = orig_raise

    # 5.2 JavaScriptError — 执行错误的 JS
    print("\n\n[5.2] 触发 JavaScriptError:")
    print("  page.run_js('throw new Error(\"demo error\")')")
    try:
        page.run_js('throw new Error("demo error for diagnostic")')
    except JavaScriptError as e:
        print(f"  捕获异常: {type(e).__name__}: {e}")
        if e.diagnostics:
            print(f"  diagnostics.error_type = {e.diagnostics.error_type}")
            print(f"  diagnostics.url        = {e.diagnostics.url}")
            print(f"\n  FailureSnapshot.summary():\n")
            print(e.diagnostics.summary())
        else:
            print("  (diagnostics 为 None — 未启用 failure_snapshot)")

    # 5.3 WaitTimeoutError — 等待元素超时
    # wait 系列方法默认不抛异常(返回 False),需临时开启 raise 模式
    # 注: waiter 工具类不包含 snapshot 集成(无需浏览器级诊断),
    #     diagnostics 在 wait_loading / _find_element / run_js / get 等页面级方法中自动附加。
    print("\n\n[5.3] 触发 WaitTimeoutError:")
    print("  Settings.raise_when_wait_failed = True")
    print("  page.wait.ele_displayed('#never-exists', timeout=2)")
    orig_wait_raise = Settings.raise_when_wait_failed
    Settings.raise_when_wait_failed = True
    try:
        page.wait.ele_displayed("#never-exists", timeout=2)
    except WaitTimeoutError as e:
        print(f"  捕获异常: {type(e).__name__}: {e}")
        print(f"  diagnostics = {e.diagnostics}")
        print("  (waiter 工具类级别的超时,不包含 snapshot)")
        print("  snapshot 自动附加场景: ele(raise_err=True) / run_js / get / wait_loading")
    finally:
        Settings.raise_when_wait_failed = orig_wait_raise


# =====================================================================
# Part 6: FailureSnapshot 数据结构独立演示
# =====================================================================

def demo_snapshot_standalone():
    sep("Part 6: FailureSnapshot 数据结构")

    snap = FailureSnapshot()
    snap.error_type = "ElementNotFoundError"
    snap.error_message = "未找到元素: #submit-btn"
    snap.url = "https://example.com/checkout"
    snap.context_id = "ctx-abc-123"
    snap.screenshot_path = "./diag/1714531200_screenshot.png"
    snap.dom_path = "./diag/1714531200_dom.html"
    snap.saved_dir = "./diag"
    snap.recent_requests = [
        {"url": "https://api.example.com/cart", "method": "GET", "status": 200},
        {"url": "https://api.example.com/pay", "method": "POST", "status": 500},
    ]
    snap.trace_entries = [
        TraceEntry("bidi_cmd", "browsingContext.navigate",
                   {"url": "https://example.com"}, elapsed_ms=320),
        TraceEntry("bidi_cmd", "script.evaluate",
                   {"expression": "document.title"}, elapsed_ms=5),
        TraceEntry("error", "element_not_found",
                   {"locator": "#submit-btn"}, status="error"),
    ]

    # summary
    print("\n[6.1] FailureSnapshot.summary():\n")
    print(snap.summary())

    # to_json
    print("\n[6.2] FailureSnapshot.to_json():\n")
    print(snap.to_json())

    # repr
    print(f"\n[6.3] repr(snap) = {repr(snap)}")


# =====================================================================
# Part 7: TraceEntry 数据结构独立演示
# =====================================================================

def demo_trace_entry_standalone():
    sep("Part 7: TraceEntry 数据结构")

    # 创建
    entry = TraceEntry(
        category="bidi_cmd",
        event="browsingContext.navigate",
        data={"url": "https://example.com", "wait": "complete"},
        context_id="ctx-abc-123",
        elapsed_ms=280.567,
        status="ok",
    )

    print(f"\n[7.1] 属性:")
    print(f"  category   = {entry.category}")
    print(f"  event      = {entry.event}")
    print(f"  data       = {entry.data}")
    print(f"  context_id = {entry.context_id}")
    print(f"  elapsed_ms = {entry.elapsed_ms}  (自动四舍五入到 2 位小数)")
    print(f"  status     = {entry.status}")
    print(f"  timestamp  = {entry.timestamp}")

    print(f"\n[7.2] to_dict():")
    print(f"  {json.dumps(entry.to_dict(), ensure_ascii=False)}")

    print(f"\n[7.3] repr():")
    print(f"  {repr(entry)}")


# =====================================================================
# Part 8: 敏感字段自动脱敏
# =====================================================================

def demo_scrub():
    sep("Part 8: 敏感字段自动脱敏")

    from ruyipage._units.tracer import _scrub_dict

    # 8.1 敏感字段
    raw = {
        "username": "admin",
        "password": "super_secret_123",
        "Cookie": "session=abc123xyz",
        "authorization": "Bearer eyJhbGciOi...",
        "url": "https://example.com/login",
        "data": "普通内容",
    }
    scrubbed = _scrub_dict(raw)
    print("\n[8.1] 敏感字段脱敏:")
    print(f"  输入: {json.dumps(raw, ensure_ascii=False)}")
    print(f"  输出: {json.dumps(scrubbed, ensure_ascii=False)}")
    print("  → password / Cookie / authorization 被替换为 '***'")

    # 8.2 超长字符串截断
    raw2 = {"script": "x" * 600}
    scrubbed2 = _scrub_dict(raw2, max_str_len=100)
    print(f"\n[8.2] 超长字符串截断 (600字符 → max_str_len=100):")
    print(f"  输出: {json.dumps(scrubbed2, ensure_ascii=False)[:120]}...")

    # 8.3 嵌套 dict
    raw3 = {
        "headers": {
            "Authorization": "Basic dXNlcjpwYXNz",
            "Content-Type": "application/json",
        }
    }
    scrubbed3 = _scrub_dict(raw3)
    print(f"\n[8.3] 嵌套脱敏:")
    print(f"  输入: {json.dumps(raw3, ensure_ascii=False)}")
    print(f"  输出: {json.dumps(scrubbed3, ensure_ascii=False)}")

    # 8.4 列表截断(最多 10 项)
    raw4 = {"items": [{"id": i} for i in range(20)]}
    scrubbed4 = _scrub_dict(raw4)
    print(f"\n[8.4] 列表截断 (20项 → 10项):")
    print(f"  输入长度: {len(raw4['items'])}")
    print(f"  输出长度: {len(scrubbed4['items'])}")


# =====================================================================
# Part 9: Settings 配置项一览
# =====================================================================

def demo_settings():
    sep("Part 9: Settings 诊断相关配置项")

    print(f"""
  Settings.trace_enabled            = {Settings.trace_enabled}
    → 是否启用 trace 记录(默认 False)

  Settings.trace_max_entries         = {Settings.trace_max_entries}
    → trace 缓冲区最大条目数(默认 1000)

  Settings.failure_snapshot_enabled  = {Settings.failure_snapshot_enabled}
    → 是否启用失败自动诊断快照(默认 False)

  Settings.snapshot_dom_max_bytes    = {Settings.snapshot_dom_max_bytes}
    → DOM 快照最大字节数(默认 2MB)

  Settings.snapshot_recent_requests  = {Settings.snapshot_recent_requests}
    → 快照中包含的最近网络请求数量(默认 30)
""")


# =====================================================================
# Part 10: FirefoxOptions 链式 API 演示
# =====================================================================

def demo_firefox_options():
    sep("Part 10: FirefoxOptions 诊断相关 API")

    opts = FirefoxOptions()

    # 链式调用
    opts.enable_trace(True)
    print(f"  opts.enable_trace(True)")
    print(f"    → opts.trace_enabled = {opts.trace_enabled}")

    opts.enable_failure_snapshot()
    print(f"  opts.enable_failure_snapshot()")
    print(f"    → opts.failure_snapshot_enabled = {opts.failure_snapshot_enabled}")

    opts.set_snapshot_dir("./my_diagnostics")
    print(f"  opts.set_snapshot_dir('./my_diagnostics')")
    print(f"    → opts.snapshot_dir = {opts.snapshot_dir}")

    # quick_start
    print(f"\n  quick_start 一键配置:")
    opts2 = FirefoxOptions()
    opts2.quick_start(trace=True, failure_snapshot=True, snapshot_dir="./diag")
    print(f"    opts.quick_start(trace=True, failure_snapshot=True, snapshot_dir='./diag')")
    print(f"    → trace_enabled          = {opts2.trace_enabled}")
    print(f"    → failure_snapshot_enabled = {opts2.failure_snapshot_enabled}")
    print(f"    → snapshot_dir           = {opts2.snapshot_dir}")


# =====================================================================
# main
# =====================================================================

def main():
    print("=" * 60)
    print("  Example 47: Debug Trace 与 Failure Snapshot 诊断演示")
    print("=" * 60)

    # --- 纯数据结构演示(不需要浏览器) ---
    demo_enable_methods()
    demo_trace_entry_standalone()
    demo_snapshot_standalone()
    demo_scrub()
    demo_settings()
    demo_firefox_options()

    # --- 需要浏览器的实战演示 ---
    server = TestServer(port=8891).start()

    # 通过 launch() 一行开启所有诊断功能
    page = launch(
        trace=True,
        failure_snapshot=True,
        snapshot_dir="./diag_output_47",
    )

    try:
        demo_trace_api(page, server)
        demo_network_trace(page, server)
        demo_trace_management(page)
        demo_failure_snapshot(page, server)

        sep("全部演示完成")
        print("""
  本例演示了 ruyipage 诊断观测系统的全部 API:

  启用方式:
    - launch(trace=True, failure_snapshot=True)
    - FirefoxOptions().enable_trace() / .enable_failure_snapshot()
    - Settings.trace_enabled = True

  Trace 时间线:
    - page.trace.enabled         → 是否启用
    - page.trace.latest(n)       → 最近 N 条 TraceEntry
    - page.trace.recent_requests(n) → 最近 N 条网络请求
    - page.trace.summary(n)      → 人类可读摘要
    - page.trace.dump_json()     → 完整 JSON 导出
    - page.trace.clear()         → 清空缓冲区

  TraceEntry:
    - .category / .event / .data / .context_id / .elapsed_ms / .status
    - .to_dict()

  FailureSnapshot:
    - .error_type / .error_message / .url / .context_id
    - .screenshot_path / .dom_path / .saved_dir
    - .recent_requests / .trace_entries / .capture_errors
    - .summary() / .to_json() / .to_dict()

  异常诊断:
    - except SomeError as e: e.diagnostics → FailureSnapshot | None

  敏感脱敏:
    - password / cookie / authorization 等字段自动替换为 ***
    - 超长字符串自动截断
""")
    finally:
        page.quit()
        server.stop()


if __name__ == "__main__":
    main()

示例测试服务器:为 network、downloads、cookies、navigation 提供本地接口

examples/test_server.py
GitHub 仓库 已同步源码 查看 GitHub 源码
tools tool
一句话定位

大量示例依赖的本地稳定环境,是测试型章节的重要配套工具。

涉及 API
TestServer.startTestServer.stopTestServer.get_url
展示的不同用法
  • 为网络拦截、下载、Cookie 等示例提供稳定服务端
  • 避免外网波动影响案例复现
适用场景

适合 testing 页面。

相关文档页面
推荐阅读
network-intercept-11downloads-23
完整代码

来源:app/content/ruyipage_examples/test_server.py · 仓库文件:test_server.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 示例测试服务器:为 network、downloads、cookies、navigation 提供本地接口
# 说明: 大量示例依赖的本地稳定环境,是测试型章节的重要配套工具。
# 你会学到:
#   1. 为网络拦截、下载、Cookie 等示例提供稳定服务端
#   2. 避免外网波动影响案例复现
# 相关 API: TestServer.start, TestServer.stop, TestServer.get_url
# =============================

# -*- coding: utf-8 -*-
"""示例用本地测试服务器。"""

import base64
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse


class TestServer(object):
    """用于 examples 的轻量 HTTP 测试服务器。"""

    def __init__(self, host="127.0.0.1", port=8888):
        self.host = host
        self.port = port
        self._server = None
        self._thread = None

    def start(self):
        if self._server:
            return self

        server = self

        class Handler(BaseHTTPRequestHandler):
            def log_message(self, format, *args):
                return

            def _cors_headers(self):
                return [
                    ("Access-Control-Allow-Origin", "*"),
                    ("Access-Control-Allow-Methods", "GET, POST, OPTIONS"),
                    (
                        "Access-Control-Allow-Headers",
                        "Content-Type, X-Ruyi-Demo, User-Agent",
                    ),
                ]

            def _write_text(self, status, body, headers=None):
                body_bytes = body.encode("utf-8")
                self.send_response(status)
                self.send_header("Content-Type", "text/plain; charset=utf-8")
                self.send_header("Content-Length", str(len(body_bytes)))
                if headers:
                    for name, value in headers:
                        self.send_header(name, value)
                self.end_headers()
                self.wfile.write(body_bytes)

            def _write_json(self, status, payload, headers=None):
                body = json.dumps(payload, ensure_ascii=False)
                body_bytes = body.encode("utf-8")
                self.send_response(status)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Content-Length", str(len(body_bytes)))
                if headers:
                    for name, value in headers:
                        self.send_header(name, value)
                self.end_headers()
                self.wfile.write(body_bytes)

            def do_GET(self):
                parsed = urlparse(self.path)
                path = parsed.path

                if path == "/set-cookie":
                    self._write_text(
                        200,
                        "cookie set ok",
                        headers=[
                            ("Set-Cookie", "server_cookie=server_value; Path=/"),
                            ("Set-Cookie", "session_id=abc123; Path=/; HttpOnly"),
                        ],
                    )
                    return

                if path == "/get-cookie":
                    cookie_header = self.headers.get("Cookie", "")
                    self._write_text(200, cookie_header or "")
                    return

                if path == "/api/data":
                    self._write_json(
                        200,
                        {
                            "status": "ok",
                            "data": {"message": "来自测试服务器的数据"},
                        },
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/headers":
                    headers = {key: value for key, value in self.headers.items()}
                    self._write_json(200, headers, headers=self._cors_headers())
                    return

                if path == "/api/collector":
                    headers = {key: value for key, value in self.headers.items()}
                    self._write_json(
                        200,
                        {
                            "status": "ok",
                            "source": "collector",
                            "message": "stable response body for data collector",
                            "headers": headers,
                        },
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/error":
                    self._write_json(
                        500,
                        {"status": "error", "message": "server error"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/mock-source":
                    self._write_json(
                        200,
                        {"status": "ok", "source": "real-server"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/slow":
                    self._write_json(
                        200,
                        {"status": "ok", "source": "slow-server"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/auth":
                    auth_header = self.headers.get("Authorization", "")
                    expected = "Basic " + base64.b64encode(b"user:pass").decode("ascii")
                    if auth_header != expected:
                        self.send_response(401)
                        self.send_header("WWW-Authenticate", 'Basic realm="RuyiTest"')
                        self.send_header("Content-Length", "0")
                        self.end_headers()
                        return

                    self._write_json(
                        200,
                        {"status": "ok", "auth": True, "user": "user"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/download/text":
                    self._write_text(
                        200,
                        "hello download",
                        headers=[
                            ("Content-Disposition", 'attachment; filename="test.txt"'),
                            ("Cache-Control", "no-store"),
                        ],
                    )
                    return

                if path == "/download/json":
                    self._write_json(
                        200,
                        {"ok": True},
                        headers=[
                            ("Content-Disposition", 'attachment; filename="test.json"'),
                            ("Cache-Control", "no-store"),
                        ],
                    )
                    return

                if path == "/nav/basic":
                    body = """<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Nav Basic</title></head>
<body><h1>Nav Basic</h1></body></html>"""
                    body_bytes = body.encode("utf-8")
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html; charset=utf-8")
                    self.send_header("Content-Length", str(len(body_bytes)))
                    self.end_headers()
                    self.wfile.write(body_bytes)
                    return

                if path == "/nav/fragment":
                    body = """<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Nav Fragment</title></head>
<body>
<h1 id='a'>A</h1>
<div style='height:1200px'></div>
<h1 id='b'>B</h1>
</body></html>"""
                    body_bytes = body.encode("utf-8")
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html; charset=utf-8")
                    self.send_header("Content-Length", str(len(body_bytes)))
                    self.end_headers()
                    self.wfile.write(body_bytes)
                    return

                if path == "/nav/history":
                    body = """<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Nav History</title></head>
<body><h1>Nav History</h1></body></html>"""
                    body_bytes = body.encode("utf-8")
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html; charset=utf-8")
                    self.send_header("Content-Length", str(len(body_bytes)))
                    self.end_headers()
                    self.wfile.write(body_bytes)
                    return

                self._write_text(404, "not found")

            def do_OPTIONS(self):
                self.send_response(204)
                for name, value in self._cors_headers():
                    self.send_header(name, value)
                self.send_header("Content-Length", "0")
                self.end_headers()

        self._server = ThreadingHTTPServer((self.host, self.port), Handler)
        self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
        self._thread.start()
        return self

    def stop(self):
        if not self._server:
            return
        self._server.shutdown()
        self._server.server_close()
        self._server = None
        self._thread = None

    def get_url(self, path="/"):
        if not path.startswith("/"):
            path = "/" + path
        return f"http://{self.host}:{self.port}{path}"

请求头抓包:真实跟踪页 POST 请求头与请求体诊断

examples/40_1_request_header_capture.py
GitHub 仓库 待同步 查看 GitHub 源码
network strict
一句话定位

面向抓包与数据采集诊断的示例,重点不是改包,而是先看清真实页面发出的 POST 请求头和请求体形状。

涉及 API
FirefoxPagepage.intercept.startInterceptedRequestreq.headersreq.bodyreq.continue_request
展示的不同用法
  • 抓取真实页面发出的 POST 请求头
  • 预览请求体内容用于逆向分析
  • 快速判断页面 URL 与接口 URL 的关系
  • 在不打断真实请求的前提下继续放行请求
适用场景

适合 network、intercept、data-collector 与 scraper 调试场景。

推荐阅读
advanced-network-18collector-28packet-capture-40

抓包采集:从真实页面请求链路定位接口数据

examples/40_scraper_packet_capture.py
GitHub 仓库 待同步 查看 GitHub 源码
network strict
一句话定位

用于 scraper 与接口采集场景的抓包示例,重点是先观察请求链路,再决定后续是监听、拦截还是 collector。

涉及 API
page.intercept.startpage.events.startpage.events.waitpage.network.add_data_collectorreq.continue_request
展示的不同用法
  • 观察真实页面请求链路
  • 定位数据接口与请求特征
  • 为后续响应体采集或 mock 做前置分析
  • 形成 scraper 场景下的网络调试起点
适用场景

适合 network、network-events、data-collector 和 scraper 文档链路。

推荐阅读
request-header-capture-40-1collector-28network-events-31

三标签页三隔离上下文:单浏览器多账号会话隔离

examples/37_three_isolated_user_context_tabs.py
GitHub 仓库 待同步 查看 GitHub 源码
browser strict
一句话定位

展示单浏览器内三个标签页分别使用不同 user context,实现 Cookie 与存储完全隔离。

涉及 API
page.browser_tools.create_user_contextpage.browser_tools.create_tabpage.browser_tools.remove_user_contextpage.contexts.close
展示的不同用法
  • 创建多个独立 user context
  • 在指定上下文中打开新标签页
  • 验证 Cookie 与存储隔离
  • 关闭标签页并清理上下文
适用场景

适合 user-contexts、browser 和 storage 页面。

相关文档页面
推荐阅读
browser-user-context-25tabs-09

代理认证与 IP 信息验证

examples/38_proxy_auth_ipinfo.py
GitHub 仓库 待同步 查看 GitHub 源码
network advanced
一句话定位

展示通过 fpfile 与代理配置协作,让业务脚本只保留最小代理入口。

涉及 API
opts.set_proxyopts.set_fpfileFirefoxPagepage.get
展示的不同用法
  • 配置 HTTP 代理地址
  • 通过 fpfile 承载代理认证账号密码
  • 访问 ipinfo 验证出口 IP
  • 降低业务脚本中明文认证信息暴露
适用场景

适合 proxy-auth、anti-bot 和 fingerprint-browser 页面。

推荐阅读
quickstart-fingerprint-browseradvanced-network-18

自动探测并接管已打开浏览器

examples/39_attach_exist_browser.py
GitHub 仓库 待同步 查看 GitHub 源码
browser advanced
一句话定位

适合 Firefox、ADS、FlowerBrowser 等已打开实例的自动探测接管场景。

涉及 API
auto_attach_exist_browser_by_processpage.browser.addresspage.titlepage.url
展示的不同用法
  • 按进程特征自动探测真实调试端口
  • 接管已打开的 Firefox 或指纹浏览器
  • 自动切到最新标签页
  • 复用已有人机操作会话
适用场景

适合 attach-existing-browser、fingerprint-browser 和 anti-bot 页面。

推荐阅读
quickstart-fingerprint-browserquickstart-private-mode

XPath Picker 综合演示

examples/42_xpath_picker_complex_showcase.py
GitHub 仓库 待同步 查看 GitHub 源码
tools advanced
一句话定位

完整展示 XPath Picker 在复杂节点、iframe、嵌套 iframe 和 open shadow root 中的拾取与代码生成能力。

涉及 API
opts.enable_xpath_pickerlaunch(xpath_picker=True)page.getpicker 自动生成代码链
展示的不同用法
  • 启用 XPath Picker 浮窗
  • 查看绝对 XPath 与相对 XPath
  • 自动生成 RuyiPage 定位代码
  • 处理 iframe 与 shadow root 嵌套结构
适用场景

适合 xpath-picker、selectors、iframe 和 shadow-dom 页面。

推荐阅读
elements-02iframe-13shadow-14

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

examples/42_2_action_visual_showcase.py
GitHub 仓库 待同步 查看 GitHub 源码
actions strict
一句话定位

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

涉及 API
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 的差异
  • 排查拖拽、点击、输入命中问题
适用场景

适合 actions、is-trusted、debug 和新手排障链路。

推荐阅读
actions-chain-05advanced-input-20js-untrusted-45

JS setter 与事件派发:对照 isTrusted 与 untrusted 行为

examples/45_js_setter_untrusted_input.py
GitHub 仓库 待同步 查看 GitHub 源码
debug strict
一句话定位

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

涉及 API
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 不能等价替代原生动作链
  • 为高风控场景排查前端事件信任问题
适用场景

适合 javascript、is-trusted、actions 和高风控调试章节。

推荐阅读
actions-chain-05advanced-input-20native-drag-35

提取 W3C WebDriver BiDi 规范 API 清单

examples/w3c_bidi/extract_w3c_bidi.py
GitHub 仓库 已同步源码 查看 GitHub 源码
tools tool
一句话定位

适合放在 BiDi 对照页,强调项目对协议演进的关注。

涉及 API
FirefoxPagepage.getpage.run_js
展示的不同用法
  • 抓取 W3C BiDi 规范目录和 API 清单
  • 为对照表和文档生成提供原始数据
适用场景

适合 bidi 页面。

相关文档页面
推荐阅读
w3c-compare
完整代码

来源:app/content/ruyipage_examples/w3c_bidi/extract_w3c_bidi.py · 仓库文件:w3c_bidi/extract_w3c_bidi.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 提取 W3C WebDriver BiDi 规范 API 清单
# 说明: 适合放在 BiDi 对照页,强调项目对协议演进的关注。
# 你会学到:
#   1. 抓取 W3C BiDi 规范目录和 API 清单
#   2. 为对照表和文档生成提供原始数据
# 相关 API: FirefoxPage, page.get, page.run_js
# =============================

# -*- coding: utf-8 -*-
"""从W3C WebDriver BiDi规范网页提取所有API(使用正确的选择器)"""

import sys
import io

if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")

from ruyipage import *
import os
import re

# 创建浏览器
page = FirefoxPage()

try:
    print("正在访问 W3C WebDriver BiDi 规范...")
    page.get("https://w3c.github.io/webdriver-bidi/")

    print("等待页面加载...")
    page.wait(3)

    print("\n从目录 (ol.toc li a) 提取所有API...")

    # 提取目录中的所有链接
    toc_script = """
    (() => {
        const items = [];
        document.querySelectorAll('ol.toc li a').forEach(link => {
            const href = link.getAttribute('href');
            const text = link.textContent.trim();
            if (href && text) {
                items.push({
                    href: href,
                    text: text,
                    id: href.replace('#', '')
                });
            }
        });
        return items;
    })()
    """

    toc_items = page.run_js(toc_script)

    print(f"\n找到 {len(toc_items)} 个目录项")

    # 分析提取命令和事件
    commands = {}
    events = {}
    modules = set()

    for item in toc_items:
        text = item["text"]
        id_str = item["id"]

        # 提取命令(格式:The module.command Command)
        cmd_match = re.search(r"The\s+(\w+)\.(\w+)\s+Command", text)
        if cmd_match:
            module = cmd_match.group(1)
            cmd_name = f"{module}.{cmd_match.group(2)}"
            modules.add(module)
            if module not in commands:
                commands[module] = []
            commands[module].append(cmd_name)
            print(f"  命令: {cmd_name}")

        # 提取事件(格式:The module.event Event)
        evt_match = re.search(r"The\s+(\w+)\.(\w+)\s+Event", text)
        if evt_match:
            module = evt_match.group(1)
            evt_name = f"{module}.{evt_match.group(2)}"
            modules.add(module)
            if module not in events:
                events[module] = []
            events[module].append(evt_name)
            print(f"  事件: {evt_name}")

    print(f"\n\n=== 统计结果 ===")
    print(f"模块数: {len(modules)}")
    print(f"命令数: {sum(len(v) for v in commands.values())}")
    print(f"事件数: {sum(len(v) for v in events.values())}")

    print("\n\n=== 按模块分类的命令 ===")
    for module in sorted(commands.keys()):
        print(f"\n{module} 模块 ({len(commands[module])} 个命令):")
        for cmd in sorted(commands[module]):
            print(f"  - {cmd}")

    print("\n\n=== 按模块分类的事件 ===")
    for module in sorted(events.keys()):
        print(f"\n{module} 模块 ({len(events[module])} 个事件):")
        for evt in sorted(events[module]):
            print(f"  - {evt}")

    # 保存结构化数据
    import json

    result = {
        "modules": sorted(list(modules)),
        "commands": commands,
        "events": events,
        "total_commands": sum(len(v) for v in commands.values()),
        "total_events": sum(len(v) for v in events.values()),
    }

    _out_path = os.path.join(os.path.dirname(__file__), "..", "w3c_bidi_apis.json")
    with open(_out_path, "w", encoding="utf-8") as f:
        json.dump(result, f, indent=2, ensure_ascii=False)

    print(f"\n\n结构化数据已保存到: {os.path.abspath(_out_path)}")

finally:
    page.quit()

生成 RuyiPage 与 W3C BiDi 规范对比表

examples/w3c_bidi/generate_comparison.py
GitHub 仓库 已同步源码 查看 GitHub 源码
tools tool
一句话定位

用于补充规范实现率和测试率的对照视图。

涉及 API
离线文档工具
展示的不同用法
  • 读取规范 API 清单
  • 生成实现率和测试率表格
  • 统计缺失 API 并输出 Markdown
适用场景

适合 bidi 页面,不作为新手入门内容。

相关文档页面
推荐阅读
w3c-extract
完整代码

来源:app/content/ruyipage_examples/w3c_bidi/generate_comparison.py · 仓库文件:w3c_bidi/generate_comparison.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 生成 RuyiPage 与 W3C BiDi 规范对比表
# 说明: 用于补充规范实现率和测试率的对照视图。
# 你会学到:
#   1. 读取规范 API 清单
#   2. 生成实现率和测试率表格
#   3. 统计缺失 API 并输出 Markdown
# 相关 API: 离线文档工具
# =============================

# -*- coding: utf-8 -*-
"""生成RuyiPage与W3C BiDi规范的精确对比表格"""

import sys
import io

if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")

import json
import os

# 读取W3C规范数据
with open("w3c_bidi_apis.json", "r", encoding="utf-8") as f:
    w3c_data = json.load(f)

# RuyiPage已实现的API
ruyipage_commands = {
    "session": [
        "session.status",
        "session.new",
        "session.end",
        "session.subscribe",
        "session.unsubscribe",
    ],
    "browser": [
        "browser.close",
        "browser.createUserContext",
        "browser.getUserContexts",
        "browser.removeUserContext",
        "browser.getClientWindows",
        "browser.setClientWindowState",
        "browser.setDownloadBehavior",
    ],
    "browsingContext": [
        "browsingContext.activate",
        "browsingContext.captureScreenshot",
        "browsingContext.close",
        "browsingContext.create",
        "browsingContext.getTree",
        "browsingContext.handleUserPrompt",
        "browsingContext.locateNodes",
        "browsingContext.navigate",
        "browsingContext.print",
        "browsingContext.reload",
        "browsingContext.setViewport",
        "browsingContext.traverseHistory",
        # 缺少: browsingContext.setBypassCSP
    ],
    "emulation": [
        "emulation.setUserAgentOverride",
        "emulation.setGeolocationOverride",
        "emulation.setTimezoneOverride",
        "emulation.setLocaleOverride",
        "emulation.setScreenOrientationOverride",
        "emulation.setScreenSettingsOverride",
        "emulation.setNetworkConditions",
        "emulation.setTouchOverride",
        # 缺少: setScriptingEnabled, setScrollbarTypeOverride, setForcedColorsModeThemeOverride
    ],
    "network": [
        "network.addIntercept",
        "network.removeIntercept",
        "network.continueRequest",
        "network.continueResponse",
        "network.continueWithAuth",
        "network.failRequest",
        "network.provideResponse",
        "network.addDataCollector",
        "network.removeDataCollector",
        "network.getData",
        "network.disownData",
        "network.setCacheBehavior",
        "network.setExtraHeaders",
    ],
    "script": [
        "script.evaluate",
        "script.callFunction",
        "script.addPreloadScript",
        "script.removePreloadScript",
        "script.getRealms",
        "script.disown",
    ],
    "storage": ["storage.getCookies", "storage.setCookie", "storage.deleteCookies"],
    "input": ["input.performActions", "input.releaseActions", "input.setFiles"],
    "webExtension": ["webExtension.install", "webExtension.uninstall"],
}

ruyipage_events = {
    "browsingContext": [
        "browsingContext.contextCreated",
        "browsingContext.contextDestroyed",
        "browsingContext.domContentLoaded",
        "browsingContext.load",
        "browsingContext.navigationStarted",
        "browsingContext.userPromptOpened",
        "browsingContext.userPromptClosed",
        # 缺少: downloadWillBegin, downloadEnd, fragmentNavigated, historyUpdated,
        #       navigationAborted, navigationCommitted, navigationFailed
    ],
    "network": [
        "network.beforeRequestSent",
        "network.responseStarted",
        "network.responseCompleted",
        "network.fetchError",
        "network.authRequired",
    ],
    "script": [
        "script.realmCreated",
        "script.realmDestroyed",
        # 缺少: script.message
    ],
    "log": ["log.entryAdded"],
    # 缺少: input.fileDialogOpened
}

# 测试覆盖情况
tested_commands = {
    "session": [
        "session.status",
        "session.new",
        "session.end",
        "session.subscribe",
        "session.unsubscribe",
    ],
    "browser": [
        "browser.close",
        "browser.createUserContext",
        "browser.getUserContexts",
        "browser.removeUserContext",
        "browser.getClientWindows",
        "browser.setClientWindowState",
    ],
    "browsingContext": [
        "browsingContext.activate",
        "browsingContext.captureScreenshot",
        "browsingContext.close",
        "browsingContext.create",
        "browsingContext.getTree",
        "browsingContext.handleUserPrompt",
        "browsingContext.locateNodes",
        "browsingContext.navigate",
        "browsingContext.print",
        "browsingContext.reload",
        "browsingContext.setViewport",
        "browsingContext.traverseHistory",
    ],
    "emulation": [],  # 未测试
    "network": [
        "network.addIntercept",
        "network.removeIntercept",
        "network.continueRequest",
        "network.continueResponse",
        "network.continueWithAuth",
        "network.failRequest",
        "network.provideResponse",
        "network.addDataCollector",
        "network.removeDataCollector",
        "network.getData",
        "network.disownData",
        "network.setCacheBehavior",
        "network.setExtraHeaders",
    ],
    "script": [
        "script.evaluate",
        "script.callFunction",
        "script.addPreloadScript",
        "script.removePreloadScript",
        "script.getRealms",
        "script.disown",
    ],
    "storage": ["storage.getCookies", "storage.setCookie", "storage.deleteCookies"],
    "input": ["input.performActions", "input.releaseActions", "input.setFiles"],
    "webExtension": [],  # 未测试
}

# 生成Markdown表格
output = []
output.append("# WebDriver BiDi API 完整对比表(基于W3C官方规范)\n")
output.append("## 📊 总体统计\n")
output.append("| 项目 | W3C规范 | RuyiPage实现 | 已测试 | 实现率 | 测试率 |")
output.append("|------|---------|-------------|--------|--------|--------|")

total_w3c_cmds = w3c_data["total_commands"]
total_w3c_evts = w3c_data["total_events"]
total_rp_cmds = sum(len(v) for v in ruyipage_commands.values())
total_rp_evts = sum(len(v) for v in ruyipage_events.values())
total_tested_cmds = sum(len(v) for v in tested_commands.values())

output.append(
    f"| 命令 | {total_w3c_cmds} | {total_rp_cmds} | {total_tested_cmds} | {total_rp_cmds / total_w3c_cmds * 100:.1f}% | {total_tested_cmds / total_w3c_cmds * 100:.1f}% |"
)
output.append(
    f"| 事件 | {total_w3c_evts} | {total_rp_evts} | {total_rp_evts} | {total_rp_evts / total_w3c_evts * 100:.1f}% | {total_rp_evts / total_w3c_evts * 100:.1f}% |"
)
output.append(
    f"| **总计** | **{total_w3c_cmds + total_w3c_evts}** | **{total_rp_cmds + total_rp_evts}** | **{total_tested_cmds + total_rp_evts}** | **{(total_rp_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%** | **{(total_tested_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%** |"
)

output.append("\n---\n")

# 按模块生成详细对比
for module in sorted(w3c_data["modules"]):
    output.append(f"\n## {module} 模块\n")

    # 命令对比
    if module in w3c_data["commands"]:
        w3c_cmds = w3c_data["commands"][module]
        rp_cmds = ruyipage_commands.get(module, [])
        tested_cmds = tested_commands.get(module, [])

        output.append(
            f"### 命令 ({len(rp_cmds)}/{len(w3c_cmds)} = {len(rp_cmds) / len(w3c_cmds) * 100:.1f}%)\n"
        )
        output.append("| W3C命令 | RuyiPage | 测试 | 状态 |")
        output.append("|---------|----------|------|------|")

        for cmd in sorted(w3c_cmds):
            implemented = "✅" if cmd in rp_cmds else "❌"
            tested = "✅" if cmd in tested_cmds else ("⚠️" if cmd in rp_cmds else "❌")
            status = (
                "完成"
                if cmd in tested_cmds
                else ("已实现未测试" if cmd in rp_cmds else "未实现")
            )
            output.append(f"| {cmd} | {implemented} | {tested} | {status} |")

    # 事件对比
    if module in w3c_data["events"]:
        w3c_evts = w3c_data["events"][module]
        rp_evts = ruyipage_events.get(module, [])

        output.append(
            f"\n### 事件 ({len(rp_evts)}/{len(w3c_evts)} = {len(rp_evts) / len(w3c_evts) * 100:.1f}%)\n"
        )
        output.append("| W3C事件 | RuyiPage | 测试 | 状态 |")
        output.append("|---------|----------|------|------|")

        for evt in sorted(w3c_evts):
            implemented = "✅" if evt in rp_evts else "❌"
            tested = "✅" if evt in rp_evts else "❌"
            status = "完成" if evt in rp_evts else "未实现"
            output.append(f"| {evt} | {implemented} | {tested} | {status} |")

    output.append("")

# 未实现的API清单
output.append("\n## 🚫 未实现的W3C API清单\n")
output.append("### 命令\n")
missing_cmds = []
for module in w3c_data["commands"]:
    w3c_cmds = w3c_data["commands"][module]
    rp_cmds = ruyipage_commands.get(module, [])
    for cmd in w3c_cmds:
        if cmd not in rp_cmds:
            missing_cmds.append(cmd)

for i, cmd in enumerate(missing_cmds, 1):
    output.append(f"{i}. ❌ **{cmd}**")

output.append("\n### 事件\n")
missing_evts = []
for module in w3c_data["events"]:
    w3c_evts = w3c_data["events"][module]
    rp_evts = ruyipage_events.get(module, [])
    for evt in w3c_evts:
        if evt not in rp_evts:
            missing_evts.append(evt)

for i, evt in enumerate(missing_evts, 1):
    output.append(f"{i}. ❌ **{evt}**")

# 未测试的API清单
output.append("\n## ⚠️ 已实现但未测试的API\n")
untested = []
for module in ruyipage_commands:
    rp_cmds = ruyipage_commands[module]
    tested_cmds = tested_commands.get(module, [])
    for cmd in rp_cmds:
        if cmd not in tested_cmds:
            untested.append(cmd)

for i, cmd in enumerate(untested, 1):
    output.append(f"{i}. ⚠️ **{cmd}**")

# 总结
output.append("\n## 🏆 总结\n")
output.append(
    f"- ✅ **命令实现率**: {total_rp_cmds}/{total_w3c_cmds} = **{total_rp_cmds / total_w3c_cmds * 100:.1f}%**"
)
output.append(
    f"- ✅ **事件实现率**: {total_rp_evts}/{total_w3c_evts} = **{total_rp_evts / total_w3c_evts * 100:.1f}%**"
)
output.append(
    f"- ✅ **总体实现率**: {total_rp_cmds + total_rp_evts}/{total_w3c_cmds + total_w3c_evts} = **{(total_rp_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%**"
)
output.append(
    f"- ✅ **测试覆盖率**: {total_tested_cmds + total_rp_evts}/{total_w3c_cmds + total_w3c_evts} = **{(total_tested_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%**"
)
output.append(
    f"- ❌ **缺失API数**: {len(missing_cmds) + len(missing_evts)} 个({len(missing_cmds)}命令 + {len(missing_evts)}事件)"
)
output.append(f"- ⚠️ **未测试API数**: {len(untested)} 个")

output.append("\n---\n")
output.append(
    "**数据来源**: W3C WebDriver BiDi 规范 (https://w3c.github.io/webdriver-bidi/)"
)
output.append("\n**生成时间**: 2026-04-06")

# 保存到文件
result_text = "\n".join(output)
_out_path = os.path.join(os.path.dirname(__file__), "W3C_BIDI_COMPARISON.md")
with open(_out_path, "w", encoding="utf-8") as f:
    f.write(result_text)

print(result_text)
print(f"\n\n对比表格已保存到: {os.path.abspath(_out_path)}")