为什么 Verdex 直接使用 CDP
Source: Dev.to
引言
Verdex 是一款开发时的创作工具,需要对 DOM 检查和 JavaScript 执行上下文进行深度、特定的控制。相对地,Playwright 是一个执行时的测试运行器,旨在实现跨浏览器的可靠性。这两种根本不同的使用场景导致了截然不同的架构选择。
关于灵感的说明
Verdex 在设计上大量借鉴了 Playwright——其可访问性树实现、隔离世界处理以及元素生命周期管理都被深入研究。Verdex 旨在与 Playwright 的精细度保持一致(ARIA 兼容的可访问性树、稳健的框架处理、隔离执行上下文),但在架构上有所区别:它为创作时的选择器构建添加了结构探索原语(get_ancestors、get_siblings、get_descendants),而不是侧重于执行时的测试可靠性。
执行时 vs 创作时:不同的问题,不同的解决方案
| 执行时 (Playwright) | 创作时 (Verdex) | |
|---|---|---|
| 选择器处理 | 每次操作都重新解析选择器 | 在多个查询之间保持稳定引用 |
| 失效元素错误 | 通过重新解析防止 | 支持多步骤 DOM 探索 |
| 跨浏览器一致性 | 必需 | 不是主要关注点 |
| 元素句柄 | 短暂的 | 会话期间的持久映射 |
Playwright 的定位器理念
Playwright 的定位器会在每次操作时自动重新解析,这可以防止失效元素错误:
“每次使用定位器执行操作时,都会在页面中定位到最新的 DOM 元素。” – Playwright 文档
对于创作时的分析,Verdex 需要在 同一个 元素上执行顺序查询:
// 创作时对同一元素的顺序查询
get_ancestors("e3"); // 从该特定元素向上遍历
get_siblings("e3", 2); // 检查同一元素的兄弟节点
get_descendants("e3"); // 探索同一元素的子节点
Playwright 的定位器每次都会重新查询 DOM,如果页面发生变化,可能会返回不同的元素。虽然 Playwright 提供了 ElementHandle 用于持久引用,但框架不鼓励使用,并在操作后自动释放它们,以强化重新解析的理念。
“等等——失效元素怎么办?”
Selenium 那臭名昭著的 StaleElementReferenceException 发生在 运行时:
element = driver.find_element(By.ID, "submit")
# ... 测试执行期间页面重新渲染 ...
element.click() # ❌ 已失效
Verdex 的持久引用仅在 创作 阶段存在,即分析静态快照时:
// 创作会话(稳定快照)
resolve_container("e3"); // 从元素向上遍历
get_siblings("e3", 2); // 检查兄弟节点
extract_anchors("e3", 1); // 查找唯一内容
// 输出:纯 Playwright 代码(自动解析)
getByTestId("product-card")
.filter({ hasText: "iPhone 15 Pro" })
.getByRole("button", { name: "Add to Cart" });
关键区别
- 生命周期阶段 – Verdex 的引用存在于静态分析期间,而非动态测试执行。
- 无页面交互 – 探索过程不会触发重新渲染。
- 安全的输出 – 生成的测试代码使用 Playwright 的自动解析定位器。
因此,Verdex 能够在多步骤结构探索中使用持久引用,而最终的测试代码则受益于 Playwright 稳健的运行时行为。
CDP 层:架构选择的重要性
Playwright 与 Verdex 都基于 Chrome DevTools Protocol (CDP)。创建隔离世界的代码看起来类似:
// Playwright 使用 CDP
const client = await page.context().newCDPSession(page);
await client.send('Page.createIsolatedWorld', {
frameId: mainFrameId,
worldName: 'verdex-bridge',
grantUniveralAccess: true
});
// Puppeteer 使用 CDP
const client = await page.createCDPSession();
const { executionContextId } = await client.send('Page.createIsolatedWorld', {
frameId: mainFrameId,
worldName: 'verdex-bridge',
grantUniveralAccess: true
});
真正的复杂性出现在跨多个操作使用元素时。
阻抗不匹配
Verdex 维护一个持久的 Map,在分析步骤之间追踪 DOM 节点。使用 Playwright + CDP 时,需要在两种对象模型之间搭桥:
- Playwright 自动管理的
ElementHandle(实用世界) - 手动管理的 CDP
objectId(隔离世界)
在它们之间转换需要额外的 evaluate 调用和上下文切换,实际上绕过了 Playwright 的抽象层,却仍要承担其跨浏览器包的开销。
为什么 Puppeteer 更自然契合
Puppeteer 直接操作 CDP 原语,保持单一抽象层级:
const session = await page.target().createCDPSession();
// 创建隔离世界
const { executionContextId } = await session.send('Page.createIsolatedWorld', {
frameId: mainFrameId,
worldName: 'verdex-bridge',
grantUniveralAccess: true
});
// 注入桥接代码
await session.send('Runtime.evaluate', {
expression: bridgeCode,
contextId: executionContextId
});
// 多步骤操作的稳定 objectId
const { result } = await session.send('Runtime.evaluate', {
expression: 'document.querySelector("[data-testid=product]")',
contextId: executionContextId
});
// 在后续调用中使用相同的 objectId
const analysis = await session.send('Runtime.callFunctionOn', {
objectId: result.objectId,
functionDeclaration: 'function() { return window.verdexBridge.fullAnalysis(this); }',
executionContextId,
returnByValue: true
});
const { result: ancestors } = await session.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return window.verdexBridge.get_ancestors(this); }',
objectId: result.objectId,
executionContextId,
returnByValue: true
});
没有阻抗不匹配、没有与自动释放的斗争,也不需要在对象模型之间搭桥。puppeteer 包(约 2 MB)是 CDP 原生的,而 playwright-core(约 10 MB)则包含了 Verdex 从未使用的跨浏览器抽象。