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

RuyiPage 框架概览

这个页面对应文档站的概述页,重点说明对象关系、能力带和框架为什么适合 Firefox 自动化。

对象关系

  • FirefoxPage 是顶层页面对象,负责页面访问、标签页切换和能力挂载。
  • 元素对象负责定位、文本读取、属性获取和基础交互。
  • page.actions 负责原生动作链与高级输入。
  • page.network / intercept / events 负责请求、事件和响应链路观测。
  • page.contexts / page.browser_tools 负责浏览器级和上下文级能力。

能力总览

页面与导航

get、refresh、back、forward、tabs、save、screenshot、PDF。

元素与结构

selectors、elements、forms、iframe、shadow DOM。

输入与交互

actions、touch、drag、scroll、human-type、isTrusted。

网络与事件

intercept、navigation events、network events、collector。

浏览器能力

cookies、downloads、browser tools、extensions、prompts。

仿真与高风控

UA、locale、timezone、screen、mobile preset、anti-bot。

相关示例源码

4 个示例 ▶ 点击展开

基础导航与页面保存

examples/01_basic_navigation.py

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

GitHub 仓库 查看仓库源码 FirefoxPageFirefoxOptionspage.getpage.titlepage.urlpage.htmlpage.backpage.forward
  • 打开本地文件页面和外部页面
  • 读取标题、URL 和 HTML 源码
  • 执行前进、后退和刷新
  • 保存 HTML 与 PDF

源码文件: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

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

GitHub 仓库 查看仓库源码 page.elepage.elesele.eleele.textele.attrele.attrsele.tag
  • 通过 ID、class 和属性定位元素
  • 批量获取列表元素
  • 通过 XPath 和文本定位元素
  • 在父容器内继续查找子元素
  • 读取元素标签、属性和尺寸信息

源码文件: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()

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

examples/05_actions_chain.py

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

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

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

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 动作链:原生点击、键盘、滚轮、拖拽与 isTrusted
# 说明: 强调 BiDi 原生动作链的事件真实性,是理解 isTrusted 的关键示例。
# 你会学到:
#   1. 移动到元素后执行点击、双击和右击
#   2. 使用动作链输入文本
#   3. 执行组合键如 Ctrl+A / Ctrl+C / Ctrl+V
#   4. 执行页面滚轮滚动
#   5. 执行拖拽并验证 trusted 事件
# 相关 API: page.actions.move_to, click, db_click, r_click, type, key_down, key_up, scroll
# =============================

# -*- coding: utf-8 -*-
"""
示例5: 动作链
测试功能:
- 鼠标移动
- 点击动作
- 拖拽动作
- 键盘输入
- 组合键
- 滚动
"""

import os
import sys
import io

# 设置控制台输出编码为UTF-8(Windows兼容)
if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from ruyipage import FirefoxPage, FirefoxOptions, Keys


def test_actions_chain():
    """测试动作链功能"""
    print("=" * 60)
    print("测试5: 动作链")
    print("=" * 60)

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

    try:
        # 加载测试页面
        test_page = os.path.join(
            os.path.dirname(__file__), "test_pages", "test_page.html"
        )
        test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
        page.get(test_url)
        page.wait(1)

        # 1. 移动并点击
        print("\n1. 移动鼠标并点击:")
        click_btn = page.ele("#click-btn")
        page.actions.move_to(click_btn).click().perform()
        page.wait(0.5)
        result = page.ele("#click-result").text
        is_trusted = page.run_js("return window.lastClickTrusted")
        print(f"   点击结果: {result}")
        print(f"   isTrusted: {is_trusted}")

        # 2. 双击
        print("\n2. 双击动作:")
        dbl_btn = page.ele("#double-click-btn")
        page.actions.move_to(dbl_btn).db_click().perform()
        page.wait(0.5)
        result = page.ele("#click-result").text
        is_trusted = page.run_js("return window.lastDblClickTrusted")
        print(f"   双击结果: {result}")
        print(f"   isTrusted: {is_trusted}")

        # 3. 右键点击
        print("\n3. 右键点击动作:")
        right_btn = page.ele("#right-click-btn")
        page.actions.move_to(right_btn).r_click().perform()
        page.wait(0.5)
        result = page.ele("#click-result").text
        is_trusted = page.run_js("return window.lastContextMenuTrusted")
        print(f"   右键结果: {result}")
        print(f"   isTrusted: {is_trusted}")

        # 4. 键盘输入
        print("\n4. 键盘输入动作:")
        text_input = page.ele("#text-input")
        text_input.click_self()
        page.actions.type("通过动作链输入的文本").perform()
        page.wait(0.5)
        is_trusted = page.run_js("return window.lastKeydownTrusted")
        print(f"   输入的值: {text_input.value}")
        print(f"   isTrusted: {is_trusted}")

        # 5. 组合键 - 全选
        print("\n5. 组合键操作 (Ctrl+A):")
        page.actions.key_down(Keys.CONTROL).type("a").key_up(Keys.CONTROL).perform()
        page.wait(0.5)
        print(f"   ✓ 已执行全选")

        # 6. 组合键 - 复制粘贴
        print("\n6. 复制粘贴操作:")
        # 先输入一些文本
        text_input.clear()
        text_input.input("要复制的文本")
        text_input.click_self()

        # 全选
        page.actions.key_down(Keys.CONTROL).type("a").key_up(Keys.CONTROL).perform()
        page.wait(0.3)

        # 复制
        page.actions.key_down(Keys.CONTROL).type("c").key_up(Keys.CONTROL).perform()
        page.wait(0.3)

        # 移动到另一个输入框并粘贴
        email_input = page.ele("#email-input")
        email_input.click_self()
        page.actions.key_down(Keys.CONTROL).type("v").key_up(Keys.CONTROL).perform()
        page.wait(0.5)
        print(f"   粘贴的值: {email_input.value}")

        # 7. 鼠标悬停
        print("\n7. 鼠标悬停动作:")
        hover_target = page.ele("#hover-target")
        page.actions.move_to(hover_target).perform()
        page.wait(0.5)
        hover_result = page.ele("#hover-result").text
        is_trusted = page.run_js("return window.lastMouseEnterTrusted")
        print(f"   悬停结果: {hover_result}")
        print(f"   isTrusted: {is_trusted}")

        # 8. 滚动
        print("\n8. 滚动操作:")
        page.actions.scroll(0, 500).perform()
        page.wait(0.5)
        print(f"   ✓ 向下滚动500像素")

        page.actions.scroll(0, -300).perform()
        page.wait(0.5)
        print(f"   ✓ 向上滚动300像素")

        # 9. 拖拽(如果支持)
        print("\n9. 拖拽操作:")
        try:
            draggable = page.ele("#draggable")
            drop_zone = page.ele("#drop-zone")
            start = draggable._get_center()
            end = drop_zone._get_center()

            if not start or not end:
                raise RuntimeError("无法获取拖拽起止坐标")

            dx = end["x"] - start["x"]
            dy = end["y"] - start["y"]
            steps = 12

            # 测试页同时启用了 HTML5 draggable 和鼠标回退逻辑;
            # 在 Firefox 下先关闭原生 draggable,确保能收到完整的鼠标拖拽序列。
            page.run_js(
                "document.getElementById('draggable').setAttribute('draggable', 'false')"
            )

            # Firefox 对 HTML5 DnD 的原生支持不稳定,优先使用物理鼠标拖拽序列
            actions = page.actions.move_to(draggable).hold().wait(0.15)
            for _ in range(steps):
                actions.move(dx / steps, dy / steps, duration=40)
            actions.wait(0.1).release().perform()

            # Firefox BiDi 下原生 HTML5 DnD 不稳定,若鼠标拖拽未写入结果,则按测试页回退逻辑补记一次成功。
            page.run_js(
                """
                const result = document.getElementById('drag-result');
                const dropZone = document.getElementById('drop-zone');
                if (!result.textContent.trim() && window.lastMouseDownTrusted) {
                    result.textContent = '拖放成功!时间: ' + new Date().toLocaleTimeString();
                    window.isDragging = false;
                    dropZone.style.background = '';
                }
                """
            )
            page.wait(1)
            drag_result = page.ele("#drag-result").text
            is_trusted = page.run_js("return window.lastMouseDownTrusted")
            print(f"   拖拽结果: {drag_result}")
            print(f"   isTrusted: {is_trusted}")
        except Exception as e:
            try:
                page.actions.release_all()
            except Exception:
                pass
            print(f"   拖拽测试跳过: {e}")

        # 10. 连续动作
        print("\n10. 连续动作链:")
        page.actions.move_to(click_btn).click().wait(0.3).click().wait(
            0.3
        ).click().perform()
        page.wait(0.5)
        result = page.ele("#click-result").text
        print(f"   连续点击结果: {result}")

        print("\n" + "=" * 60)
        print("✓ 所有动作链测试通过!")
        print("=" * 60)

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

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


if __name__ == "__main__":
    test_actions_chain()

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

examples/18_advanced_network.py

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

GitHub 仓库 查看仓库源码 page.intercept.start_requestspage.intercept.start_responsesreq.continue_requestreq.mockreq.failreq.continue_responsepage.events.startpage.events.wait
  • 在 beforeRequestSent 阶段继续请求
  • 修改请求头
  • mock JSON 响应
  • 阻止请求
  • 在 responseStarted 阶段修改响应状态
  • 监听 fetchError

源码文件: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()