fix(cdp,js): playwright actionability stubs and objectId resolve (#178)

Playwright fails locator actions on Obscura for two avoidable reasons
that are separate from the missing layout engine. Both have trivial
fixes that unblock the actionability polling loop.

- Element.checkVisibility(opts) was undefined. Playwright >= 1.25 calls
  it before every input event and retries until its action timeout if
  the method isn't a function. Add a stub that returns true. Without a
  layout pass we can't compute it for real, but a permissive stub still
  beats the undefined-function path.

- ARIA reflection properties were missing on Element (role, ariaLabel,
  ariaSelected, ariaExpanded, ariaHidden, ariaChecked, ariaDisabled,
  ariaRoleDescription). Playwright's getByRole and getByLabel locators
  read them. Reflect through to the underlying aria-* attributes for
  both read and write.

- DOM.getBoxModel and DOM.getContentQuads ignored the objectId param.
  Playwright passes only objectId (from a prior DOM.resolveNode), and
  the handlers silently defaulted to node 0, so dispatchMouseEvent
  always clicked the wrong element. Extract a resolve_node_id helper
  that honors nodeId, backendNodeId, and objectId in that order.

- Page.captureScreenshot and Page.captureSnapshot now return a clear
  "not supported, use a real Chromium for the screenshot leg" error
  instead of the generic "Unknown Page method" fallback. Matches the
  pattern already in place for Page.printToPDF.

Partial fix for #45. The architectural blocker (no layout engine, so
multi-element click coordinates are wrong) is unchanged.
This commit is contained in:
SGavrl authored and GitHub committed 2026-05-24 20:13:43 +00:00
1 parent 3d12f76d8e
commit a3b21bbb70
4 files changed
+155 -6

No files matched your search

+30 -6
View File
@@ -1,8 +1,36 @@
use obscura_browser::Page;
use obscura_dom::{DomTree, NodeData, NodeId};
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
/// Resolve a DOM `nodeId` from CDP params. Honors `nodeId`, `backendNodeId`,
/// and `objectId` in that order. Playwright commonly passes only `objectId`
/// (returned by a prior `DOM.resolveNode`); without this fallback those
/// requests silently default to node 0 and click the wrong element.
fn resolve_node_id(page: &mut Page, params: &Value) -> Result<u64, String> {
if let Some(nid) = params.get("nodeId").and_then(|v| v.as_u64()) {
return Ok(nid);
}
if let Some(nid) = params.get("backendNodeId").and_then(|v| v.as_u64()) {
return Ok(nid);
}
if let Some(oid) = params.get("objectId").and_then(|v| v.as_str()) {
let code = format!(
"(function() {{ var o = globalThis.__obscura_objects && globalThis.__obscura_objects['{}']; \
return (o && typeof o._nid === 'number') ? o._nid : -1; }})()",
oid.replace('\'', "\\'")
);
let result = page.evaluate(&code);
let nid = result.as_f64().map(|n| n as i64).unwrap_or(-1);
if nid < 0 {
return Err(format!("objectId {oid} could not be resolved to a node"));
}
return Ok(nid as u64);
}
Err("nodeId, backendNodeId, or objectId required".to_string())
}
pub async fn handle(
method: &str,
params: &Value,
@@ -138,10 +166,8 @@ pub async fn handle(
"setAttributeValue" => Ok(json!({})),
"removeNode" => Ok(json!({})),
"getBoxModel" => {
let node_id = params.get("nodeId").and_then(|v| v.as_u64())
.or_else(|| params.get("backendNodeId").and_then(|v| v.as_u64()))
.unwrap_or(0);
let page = ctx.get_session_page_mut(session_id).ok_or("No page")?;
let node_id = resolve_node_id(page, params)?;
let code = format!(
"(function() {{\
var el = globalThis._wrap && globalThis._wrap({0});\
@@ -175,10 +201,8 @@ pub async fn handle(
}))
}
"getContentQuads" => {
let node_id = params.get("nodeId").and_then(|v| v.as_u64())
.or_else(|| params.get("backendNodeId").and_then(|v| v.as_u64()))
.unwrap_or(0);
let page = ctx.get_session_page_mut(session_id).ok_or("No page")?;
let node_id = resolve_node_id(page, params)?;
let code = format!(
"(function() {{\
var el = globalThis._wrap && globalThis._wrap({0});\