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

RuyiPage 示例中心

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

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

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

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

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

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

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

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