最接近真实业务脚本的入门示例,展示如何从搜索框输入到结果列表提取。
- 打开搜索引擎主页并定位输入框
- 输入关键词并使用回车提交
- 批量读取搜索结果列表
- 在结果容器内二次定位标题、链接和摘要
- 跨页面翻页抓取结果
适合第一次把 API 串起来,理解页面访问、元素查找、动作链和数据提取如何组合成完整业务脚本。
这里不是简单的文件清单,而是将 RuyiPage 仓库中的 examples 渲染成案例卡。每个例子都带有文件名、分类、难度、不同用法、涉及 API、适用场景与相关页面,并提供 GitHub 源码入口,帮助你从“例子”回到“文档”,也从“文档”回到“例子”。
最接近真实业务脚本的入门示例,展示如何从搜索框输入到结果列表提取。
适合第一次把 API 串起来,理解页面访问、元素查找、动作链和数据提取如何组合成完整业务脚本。
来源:app/content/ruyipage_examples/quickstart_bing_search.py · 仓库文件:quickstart_bing_search.py
# =============================
# RuyiPage 示例注释版
# 标题: Bing 搜索并抓取前三页结果
# 说明: 最接近真实业务脚本的入门示例,展示如何从搜索框输入到结果列表提取。
# 你会学到:
# 1. 打开搜索引擎主页并定位输入框
# 2. 输入关键词并使用回车提交
# 3. 批量读取搜索结果列表
# 4. 在结果容器内二次定位标题、链接和摘要
# 5. 跨页面翻页抓取结果
# 相关 API: FirefoxOptions, FirefoxPage, page.get, page.ele, page.eles, page.actions.press, ele.attr, ele.text
# =============================
# -*- coding: utf-8 -*-
"""快速开始:Bing 搜索前 3 页。"""
from ruyipage import FirefoxOptions, FirefoxPage, Keys
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:
page.get("https://cn.bing.com/")
page.ele("#sb_form_q").input("小肩膀教育")
page.actions.press(Keys.ENTER).perform()
page.wait(3)
for page_no in range(1, 4):
print("=" * 80)
print(f"第 {page_no} 页")
print("=" * 80)
items = page.eles("css:#b_results > li.b_algo")
for i, item in enumerate(items, 1):
title_ele = item.ele("css:h2 a")
if not title_ele:
continue
title = " ".join((title_ele.text or "").split())
url = title_ele.attr("href") or ""
desc_ele = item.ele("css:.b_caption p")
content = desc_ele.text if desc_ele else item.text
content = " ".join((content or "").split())
print(f"{i}. {title}")
print(f" URL: {url}")
print(f" 内容: {content}")
if page_no < 3:
next_btn = page.ele("css:a.sb_pagN")
if not next_btn:
break
next_btn.click_self()
page.wait(2)
finally:
page.quit()
# -*- coding: utf-8 -*-
"""快速开始:Bing 搜索前 3 页。"""
from ruyipage import FirefoxOptions, FirefoxPage, Keys
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:
page.get("https://cn.bing.com/")
page.ele("#sb_form_q").input("小肩膀教育")
page.actions.press(Keys.ENTER).perform()
page.wait(3)
for page_no in range(1, 4):
print("=" * 80)
print(f"第 {page_no} 页")
print("=" * 80)
items = page.eles("css:#b_results > li.b_algo")
for i, item in enumerate(items, 1):
title_ele = item.ele("css:h2 a")
if not title_ele:
continue
title = " ".join((title_ele.text or "").split())
url = title_ele.attr("href") or ""
desc_ele = item.ele("css:.b_caption p")
content = desc_ele.text if desc_ele else item.text
content = " ".join((content or "").split())
print(f"{i}. {title}")
print(f" URL: {url}")
print(f" 内容: {content}")
if page_no < 3:
next_btn = page.ele("css:a.sb_pagN")
if not next_btn:
break
next_btn.click_self()
page.wait(2)
finally:
page.quit()
面向高风控站点的快速开始示例,覆盖挑战处理、输入框识别和 Cookie 提取。
适合 anti-bot、Firefox 指纹环境说明和真实站点实战章节。
来源:app/content/ruyipage_examples/quickstart_cloudfare.py · 仓库文件:quickstart_cloudfare.py
# =============================
# 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()
# -*- 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()
演示如何把 Firefox 指纹浏览器、指纹文件、地理位置、语言和请求头一起接入 RuyiPage。
适合高风控、跨地区语言环境模拟和指纹浏览器接入文档。
展示 Firefox 私密浏览模式的两种启动方式,以及它与临时 profile 的区别。
适合新手理解浏览器启动模式、临时会话和隐私场景。
最短可运行的 hello-world 级别示例,适合验证环境和建立第一层心智模型。
适合安装页后的第一段示例代码。
来源:app/content/ruyipage_examples/01_quickstart.py · 仓库文件:01_quickstart.py
# =============================
# 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()
# -*- 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()
覆盖最基础的页面访问、前进后退和保存 HTML/PDF。
适合 page、navigation、pdf 页面共同引用。
来源:app/content/ruyipage_examples/01_basic_navigation.py · 仓库文件:01_basic_navigation.py
# =============================
# 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()
# -*- 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()
系统讲透单元素、多元素、文本、XPath 和属性定位的入门示例。
适合 selectors 与 elements 文档页。
来源:app/content/ruyipage_examples/02_element_finding.py · 仓库文件:02_element_finding.py
# =============================
# 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()
# -*- 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()
围绕元素对象的高层交互,覆盖输入、点击、选择和状态读取。
适合 forms、elements 和 actions 的过渡章节。
来源:app/content/ruyipage_examples/03_element_interaction.py · 仓库文件:03_element_interaction.py
# =============================
# 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()
# -*- 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()
用于建立稳定脚本的等待心智,比硬编码 sleep 更适合文档新手章节。
适合 waits 页面,也常作为动态页面调试的前置阅读。
来源:app/content/ruyipage_examples/04_wait_conditions.py · 仓库文件:04_wait_conditions.py
# =============================
# 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()
# -*- 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()
强调 BiDi 原生动作链的事件真实性,是理解 isTrusted 的关键示例。
适合 actions、is-trusted 和 drag-drop 页面。
来源:app/content/ruyipage_examples/05_actions_chain.py · 仓库文件:05_actions_chain.py
# =============================
# 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()
# -*- 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()
覆盖最常用的页面与元素截图输出能力。
适合 screenshots 页面和综合示例的关联阅读。
来源:app/content/ruyipage_examples/06_screenshot.py · 仓库文件:06_screenshot.py
# =============================
# 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()
# -*- 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()
展示 run_js、参数传递、DOM 修改和 preload script 的调试价值。
适合 javascript、script 和 preload-scripts 页面。
来源:app/content/ruyipage_examples/07_javascript.py · 仓库文件:07_javascript.py
# =============================
# 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()
# -*- 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()
围绕登录态和会话管理的核心示例。
适合 cookies 与 browser 页面。
来源:app/content/ruyipage_examples/08_cookies.py · 仓库文件:08_cookies.py
# =============================
# 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()
# -*- 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()
展示 tab 作为日常页面操作的重要组织单位。
适合 tabs、browser 和 browsing-context 页面。
来源:app/content/ruyipage_examples/09_tabs.py · 仓库文件:09_tabs.py
# =============================
# 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()
# -*- 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()
围绕滚轮和滚动可见性组织的一组常见交互。
适合 scrolling 页面,也可作为 actions 的子案例。
来源:app/content/ruyipage_examples/10_scrolling.py · 仓库文件:10_scrolling.py
# =============================
# 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()
# -*- 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()
通过本地 TestServer 让请求拦截、mock 和 fail 行为可重复验证。
适合 network 与 intercept 页面。
来源:app/content/ruyipage_examples/11_network_intercept.py · 仓库文件:11_network_intercept.py
# =============================
# 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()
# -*- 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()
演示如何把 console 作为页面调试和自动化观测的一部分。
适合 console 和 events 章节。
来源:app/content/ruyipage_examples/12_console_listener.py · 仓库文件:12_console_listener.py
# =============================
# 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()
# -*- 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 专题的主示例,必须在文档中单列。
适合 iframe 页面与复杂页面结构专题。
来源:app/content/ruyipage_examples/13_iframe.py · 仓库文件:13_iframe.py
# =============================
# 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()
# -*- 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 与复杂边界专题的主示例,必须在文档中单列。
适合 shadow-dom 和 complex-boundaries 专题页面。
来源:app/content/ruyipage_examples/14_shadow_dom.py · 仓库文件:14_shadow_dom.py
# =============================
# 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()
# -*- 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 顶部重点展示。
来源:app/content/ruyipage_examples/15_comprehensive.py · 仓库文件:15_comprehensive.py
# =============================
# 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()
# -*- 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()
系统覆盖 window 与 viewport 的浏览器级操作。
适合 window 与 browser 页面。
来源:app/content/ruyipage_examples/16_window_management.py · 仓库文件:16_window_management.py
# =============================
# 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()
# -*- 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()
专门讲 alert、confirm、prompt 及其自动策略。
适合 prompts 页面。
来源:app/content/ruyipage_examples/17_user_prompts.py · 仓库文件:17_user_prompts.py
# =============================
# 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()
# -*- 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()
展示 intercept 与 events 组合后的完整网络控制能力。
适合 network、intercept 和 anti-bot 章节。
来源:app/content/ruyipage_examples/18_advanced_network.py · 仓库文件:18_advanced_network.py
# =============================
# 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()
# -*- 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()
集中说明 save_pdf 的参数覆盖能力。
适合 pdf 页面。
来源:app/content/ruyipage_examples/19_pdf_printing.py · 仓库文件:19_pdf_printing.py
# =============================
# 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()
# -*- 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()
输入能力大全示例,是 actions、touch、uploads 和 isTrusted 的主参考。
适合 actions、touch、drag-drop、forms 和 is-trusted 页面。
来源:app/content/ruyipage_examples/20_advanced_input.py · 仓库文件:20_advanced_input.py
# =============================
# 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)
# -*- 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 在 Firefox 路线上的主要能力边界。
适合 emulation、mobile 和 anti-bot 页面。
来源:app/content/ruyipage_examples/21_emulation.py · 仓库文件:21_emulation.py
#!/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()
#!/usr/bin/env python
# -*- 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()
把移动端预设能力放到真实站点中验证,是 mobile 页的主示例。
适合 mobile、emulation 和 anti-bot 展示。
来源:app/content/ruyipage_examples/21_mobile_google_emulation.py · 仓库文件:21_mobile_google_emulation.py
# =============================
# 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()
# -*- 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()
覆盖扩展安装、验证注入和卸载,是浏览器级能力的重要专题。
适合 extensions 页面。
来源:app/content/ruyipage_examples/22_web_extension.py · 仓库文件:22_web_extension.py
#!/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()
#!/usr/bin/env python
# -*- 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()
讲清下载事件链和文件落盘验证,是 downloads 页的主示例。
适合 downloads 与 events 页面。
来源:app/content/ruyipage_examples/23_download.py · 仓库文件:23_download.py
#!/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()
#!/usr/bin/env python
# -*- 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()
导航事件链路的标准参考示例。
适合 navigation-events 页面。
来源:app/content/ruyipage_examples/24_navigation_events.py · 仓库文件:24_navigation_events.py
#!/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()
#!/usr/bin/env python
# -*- 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 模块最完整的 user context 与 client window 说明案例。
适合 browser、user-contexts 和 window 页面。
来源:app/content/ruyipage_examples/25_browser_user_context.py · 仓库文件:25_browser_user_context.py
#!/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()
#!/usr/bin/env python
# -*- 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()
是 browsing context 章节的主参考示例。
适合 browsing-context 页面。
来源:app/content/ruyipage_examples/26_browsing_context_advanced.py · 仓库文件:26_browsing_context_advanced.py
#!/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()
#!/usr/bin/env python
# -*- 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 的高级能力和 Firefox 当前支持情况。
适合 emulation 的高级能力章节。
来源:app/content/ruyipage_examples/27_emulation_advanced.py · 仓库文件:27_emulation_advanced.py
#!/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()
#!/usr/bin/env python
# -*- 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()
用于说明 add_data_collector 在数据抓取和协议观测中的价值。
适合 data-collector 与 network 页面。
来源:app/content/ruyipage_examples/28_network_data_collector.py · 仓库文件:28_network_data_collector.py
#!/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()
#!/usr/bin/env python
# -*- 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 的高级能力,是 script 页的重要支撑示例。
适合 script 和 forms 页面。
来源:app/content/ruyipage_examples/29_script_input_advanced.py · 仓库文件:29_script_input_advanced.py
#!/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()
#!/usr/bin/env python
# -*- 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()
把 context 事件和 prompt 事件统一放到协议事件角度理解。
适合 browsing-context、events 与 prompts 页面。
来源:app/content/ruyipage_examples/30_browsing_context_events.py · 仓库文件:30_browsing_context_events.py
#!/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()
#!/usr/bin/env python
# -*- 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()
是 network-events 页最直接的主示例。
适合 network-events 与 auth 页面。
来源:app/content/ruyipage_examples/31_network_events.py · 仓库文件:31_network_events.py
#!/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()
#!/usr/bin/env python
# -*- 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()
用于说明 realms 和 preload script 如何和事件订阅配合。
适合 script 和 preload-scripts 页面。
来源:app/content/ruyipage_examples/32_script_events.py · 仓库文件:32_script_events.py
#!/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()
#!/usr/bin/env python
# -*- 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()
把日志和输入事件统一纳入事件系统理解。
适合 console、events 和 forms 页面。
来源:app/content/ruyipage_examples/33_log_input_events.py · 仓库文件:33_log_input_events.py
#!/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()
#!/usr/bin/env python
# -*- 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()
把一些散落但重要的能力集中演示,适合做协议能力查漏补缺。
适合 strict 与 testing 章节。
来源:app/content/ruyipage_examples/34_remaining_commands.py · 仓库文件:34_remaining_commands.py
#!/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()
#!/usr/bin/env python
# -*- 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()
专注于高层原生 drag 能否正确触发真实 drop 事件。
适合 drag-drop 与 is-trusted 页面。
来源:app/content/ruyipage_examples/35_native_bidi_drag.py · 仓库文件:35_native_bidi_drag.py
# =============================
# 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()
# -*- 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 的焦点、键盘与 trusted change 路径问题。
适合 forms、actions 和 strict 示例讲解。
来源:app/content/ruyipage_examples/36_native_bidi_select.py · 仓库文件:36_native_bidi_select.py
# =============================
# 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()
# -*- 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()
完整演示 ruyipage 的调试诊断与观测系统:Trace 时间线记录、FailureSnapshot 失败自动快照、异常的 .diagnostics 属性和敏感字段自动脱敏。
适合调试排错、生产环境诊断和自动化测试失败分析。
来源:app/content/ruyipage_examples/47_trace_and_diagnostics.py · 仓库文件:47_trace_and_diagnostics.py
# =============================
# 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()
# -*- 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()
大量示例依赖的本地稳定环境,是测试型章节的重要配套工具。
适合 testing 页面。
来源:app/content/ruyipage_examples/test_server.py · 仓库文件:test_server.py
# =============================
# 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}"
# -*- 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 请求头和请求体形状。
适合 network、intercept、data-collector 与 scraper 调试场景。
用于 scraper 与接口采集场景的抓包示例,重点是先观察请求链路,再决定后续是监听、拦截还是 collector。
适合 network、network-events、data-collector 和 scraper 文档链路。
展示单浏览器内三个标签页分别使用不同 user context,实现 Cookie 与存储完全隔离。
适合 user-contexts、browser 和 storage 页面。
展示通过 fpfile 与代理配置协作,让业务脚本只保留最小代理入口。
适合 proxy-auth、anti-bot 和 fingerprint-browser 页面。
适合 Firefox、ADS、FlowerBrowser 等已打开实例的自动探测接管场景。
适合 attach-existing-browser、fingerprint-browser 和 anti-bot 页面。
完整展示 XPath Picker 在复杂节点、iframe、嵌套 iframe 和 open shadow root 中的拾取与代码生成能力。
适合 xpath-picker、selectors、iframe 和 shadow-dom 页面。
用于排查自动化脚本里“鼠标到底移到了哪里、点击是否命中、JS click 与原生动作有什么区别”的可视化诊断示例。
适合 actions、is-trusted、debug 和新手排障链路。
通过普通 JS 事件与带 ruyi=true 的扩展事件做对照,帮助你理解为什么前端 dispatchEvent 默认不是 trusted 行为。
适合 javascript、is-trusted、actions 和高风控调试章节。
适合放在 BiDi 对照页,强调项目对协议演进的关注。
适合 bidi 页面。
来源:app/content/ruyipage_examples/w3c_bidi/extract_w3c_bidi.py · 仓库文件:w3c_bidi/extract_w3c_bidi.py
#!/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()
#!/usr/bin/env python
# -*- 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()
用于补充规范实现率和测试率的对照视图。
适合 bidi 页面,不作为新手入门内容。
来源:app/content/ruyipage_examples/w3c_bidi/generate_comparison.py · 仓库文件:w3c_bidi/generate_comparison.py
#!/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)}")
#!/usr/bin/env python
# -*- 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)}")