Why Verdex Uses CDP Directly
Source: Dev.to
Introduction
Verdex is a development‑time authoring tool that needs deep, specific control over DOM inspection and JavaScript execution contexts. Playwright, on the other hand, is an execution‑time test runner optimized for cross‑browser reliability. These fundamentally different use cases drive distinct architectural choices.
A Note on Inspiration
Verdex owes a significant debt to Playwright’s design—its accessibility‑tree implementation, isolated‑world handling, and element‑lifecycle management were studied closely. Verdex aims for parity with Playwright’s sophistication (ARIA‑compliant accessibility trees, robust frame handling, isolated execution contexts) but diverges in architecture: it adds structural‑exploration primitives (get_ancestors, get_siblings, get_descendants) for authoring‑time selector construction rather than execution‑time test reliability.
Execution‑Time vs Authoring‑Time: Different Problems, Different Solutions
| Execution‑Time (Playwright) | Authoring‑Time (Verdex) | |
|---|---|---|
| Selector handling | Re‑resolve selectors on every action | Maintain stable references across multiple queries |
| Stale element bugs | Prevented by re‑resolution | Enable multi‑step DOM exploration |
| Cross‑browser uniformity | Required | Not a primary concern |
| Element handles | Ephemeral | Persistent mappings during a session |
Playwright’s Locator Philosophy
Playwright’s Locators automatically re‑resolve on each action, which protects against stale‑element bugs:
“Every time a locator is used for an action, an up‑to‑date DOM element is located in the page.” – Playwright docs
For authoring‑time analysis Verdex needs to perform sequential queries on the same element:
// Sequential queries on the same element during authoring
get_ancestors("e3"); // Walk up from this specific element
get_siblings("e3", 2); // Examine the SAME element's siblings
get_descendants("e3"); // Explore the SAME element's children
Playwright’s Locators would re‑query the DOM each time, potentially returning different elements if the page changes. While Playwright does provide ElementHandles for persistent references, the framework discourages their use and auto‑disposes them after actions, reinforcing the re‑resolution philosophy.
“But Wait—What About Stale Elements?”
Selenium’s infamous StaleElementReferenceException occurs at runtime:
element = driver.find_element(By.ID, "submit")
# ... page re‑renders during test execution ...
element.click() # ❌ Stale!
Verdex’s persistent references exist only during authoring, i.e., while analyzing a static snapshot:
// Authoring session (stable snapshot)
resolve_container("e3"); // Walk up from element
get_siblings("e3", 2); // Check siblings
extract_anchors("e3", 1); // Find unique content
// Output: Pure Playwright code (auto‑resolving)
getByTestId("product-card")
.filter({ hasText: "iPhone 15 Pro" })
.getByRole("button", { name: "Add to Cart" });
Key differences
- Lifecycle phase – Verdex references exist during static analysis, not dynamic test execution.
- No page interactions – Exploration does not trigger re‑renders.
- Safe output – Generated test code uses Playwright’s auto‑resolving Locators.
Thus Verdex enjoys persistent references for multi‑step structural exploration while the final test code benefits from Playwright’s robust runtime behavior.
The CDP Layer: Where Architecture Choices Matter
Both Playwright and Verdex build on the Chrome DevTools Protocol (CDP). Creating an isolated world looks similar:
// Playwright with CDP
const client = await page.context().newCDPSession(page);
await client.send('Page.createIsolatedWorld', {
frameId: mainFrameId,
worldName: 'verdex-bridge',
grantUniveralAccess: true
});
// Puppeteer with CDP
const client = await page.createCDPSession();
const { executionContextId } = await client.send('Page.createIsolatedWorld', {
frameId: mainFrameId,
worldName: 'verdex-bridge',
grantUniveralAccess: true
});
The complexity arises when working with elements across multiple operations.
The Impedance Mismatch
Verdex maintains a persistent Map that tracks DOM nodes across analysis steps. When using Playwright + CDP you bridge two object models:
- Playwright’s auto‑managed
ElementHandles (utility world) - Manually managed CDP
objectIds (isolated world)
Converting between them requires extra evaluation calls and context switches, effectively bypassing Playwright’s abstractions while still paying the cost of its cross‑browser bundle.
Why Puppeteer Fits Naturally
Puppeteer operates directly on CDP primitives, keeping a single abstraction level:
const session = await page.target().createCDPSession();
// Create isolated world
const { executionContextId } = await session.send('Page.createIsolatedWorld', {
frameId: mainFrameId,
worldName: 'verdex-bridge',
grantUniveralAccess: true
});
// Inject bridge code
await session.send('Runtime.evaluate', {
expression: bridgeCode,
contextId: executionContextId
});
// Stable objectId for multi‑step operations
const { result } = await session.send('Runtime.evaluate', {
expression: 'document.querySelector("[data-testid=product]")',
contextId: executionContextId
});
// Use the same objectId across calls
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
});
No impedance mismatch, no fighting auto‑disposal, and no need to bridge between object models. The puppeteer package (~2 MB) is CDP‑native, whereas playwright-core (~10 MB) includes cross‑browser abstractions that Verdex never uses.