Addresses follow-up issues from PR #44 bot review:
- Add parentId field to AXNode (walk DOM ancestors to resolve AX parent)
- Omit optional fields when empty (name, value, properties, childIds)
instead of emitting null/empty arrays, per CDP spec
- Add ax_value_boolean() and ax_value_integer() helpers for consistent
AXValue construction across all property types
- Use typed helpers throughout compute_properties() instead of inline
JSON construction
CDP spec reference: Accessibility.AXNode requires parentId as AXNodeId,
and optional fields should be absent when not applicable.
Co-authored-by: zhangyang-crazy-one <[email protected]>
Fetch.enable was setting page.intercept_enabled to false and clearing
intercept_block_patterns, which caused should_block_url() to always
return false. As a result, Fetch.requestPaused events were never
triggered and Playwright's page.route() handlers never executed.
- Set intercept_enabled to true (was false)
- Propagate URL patterns to page.intercept_block_patterns (was cleared)
Fixes#49
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Per qodo-ai-reviewer feedback on PR #44: Accessibility.getFullAXTree
was emitting AXNode.role as a plain string and name/value as plain
strings or null, instead of CDP-compliant AXValue objects.
Changes:
- Add ax_value_role() and ax_value_string() helpers
- Change compute_name()/compute_value() to return Option<String>
- Wrap role, name, value as AXValue in build_ax_node()
The properties array already used the correct AXValue format.
Brings Obscura's CDP server in line with what Playwright's
`chromium.connectOverCDP()` expects. Each fix is small and surgical;
together they make `connect → newContext → newPage → goto → page.title()`
work end-to-end against `https://example.com`.
1. **server.rs — emit pending events before command response.**
Chromium CDP semantics require side-effect events (e.g.
`Target.targetCreated` + `Target.attachedToTarget` from
`Target.createTarget`) to arrive *before* the originating command's
response. Playwright's wire reader assumes this ordering — it
resolves the response promise on the response and then synchronously
looks up the freshly-created session. Reversing the order makes that
lookup miss and the page hang at `newPage()`.
2. **domains/browser.rs — accept Browser.setWindowBounds as a no-op.**
Playwright calls `Browser.setWindowBounds` during teardown. Returning
"Unknown Browser method" causes Playwright to treat the target as
broken and tear it down mid-test, surfacing as "Frame has been
detached" on the next `page.title()`.
3. **obscura-browser/page.rs — frame_id == target_id for main frame.**
Was `format!("{id}.1", id)`. Playwright's frame manager indexes
frames by frame id and looks up the main frame by target id, so the
`.1` suffix made every main-frame lookup miss.
4. **domains/page.rs + dispatch.rs — track isolated worlds and re-emit
them post-navigation.** Playwright registers a utility world via
`Page.createIsolatedWorld` and expects `Runtime.executionContextCreated`
for each registered world after every navigation. Without re-emission,
the utility-world handle goes stale post-`goto` and `page.title()`
hangs waiting for the world's execution context. Falls back to the
legacy `__puppeteer_utility_world__24.40.0` name when no worlds have
been registered, preserving prior Puppeteer support.
5. **obscura-js/runtime.rs — strip trailing semicolons before wrapping.**
Playwright's utility-script expression is an IIFE ending in `})();`.
Both `wrap_expression` (CDP `Runtime.evaluate`) and `evaluate_for_cdp`
(the `meta_code` IIFE) embed the user expression inside `({EXPR})` /
`return ({EXPR});`. With the trailing `;` left in, this produces
`({...;})` — a SyntaxError. Parse errors are not catchable, so the
surrounding try/catch never fires and the call silently returns
`undefined`, surfacing as `page.title() === ""`.
Verified end-to-end against `https://example.com`:
[probe] connect OK
[probe] newContext OK
[probe] newPage OK
[probe] goto OK
[probe] title = Example Domain