Commit Graph
7 Commits
Author SHA1 Message Date
zhangyang-crazy-one 5d01b5eb6a fix(cdp): implement triple-click select-all for text inputs (#316 follow-up) (#320)
Passing detail:clickCount to MouseEvent is necessary but not sufficient
for triple-click to select all text. The select-all-on-triple-click is a
browser native behavior that synthetic MouseEvent dispatch doesn't trigger.

Add manual selection after click dispatch: when clickCount >= 3 and the
target is an INPUT or TEXTAREA, set selectionStart=0 and
selectionEnd=value.length.
2026-06-23 13:26:18 +02:00
zhangyang-crazy-one cd0fbc02c2 fix(cdp): pass clickCount to MouseEvent.detail + fix char handler event loop (#316) (#317)
Bug 1: clickCount parameter was read with underscore prefix and silently
discarded. MouseEvent constructor never received the detail field, so
triple-click (clickCount:3) was always treated as a single click by the
DOM — inputs never selected their full text.

Fix: rename _click_count to click_count, pass it as detail in both
mousedown and click MouseEvent constructors.

Bug 2: page.type() intermittently dropped the last character because
the char handler's quote-escaping order was wrong (single-quote before
backslash), and the V8 event loop wasnt pumped after dispatching the
input event.

Fix: fix escaping order (backslash first, then single-quote), and add
page.settle(50) after the char handler's evaluate to pump the event
loop.
2026-06-22 11:01:40 +00:00
zhangyang-crazy-oneandSGavrl 5225f6d985 fix(js): serialize dynamic module imports + re-land base href for SPA lazy chunks (#246) (#275)
* fix(js): serialize dynamic module imports + re-land base href for SPA lazy chunks (#246)

Add a serialized import queue (__dynScriptQueue + __processDynScriptQueue)
in bootstrap.js so that dynamically inserted <script module> tags execute
one at a time rather than concurrently. This prevents re-entrant RefCell
panics in deno_core's futures_unordered_driver when SPAs (Angular, etc.)
append multiple lazy chunks at once.

Also re-land dynamic import <base href> resolution (previously reverted
as 88af239). The original panic was caused by concurrency, not the URL
resolution logic — now safe because imports are serialized.

Inline module scripts also route through the same queue for consistency.

Fixes #246
Closes #242 (dynamic import base href)

* Fix base-href resolution, queue wedge, and double-fired load event

- Resolve <base href> to an absolute URL before resolving the script src, and
  guard the new URL() call. A path-only base like <base href="/"> (the standard
  Angular form) was passed raw to new URL(src, base), which threw "Invalid URL"
  out of appendChild and broke the SPAs this change targets.
- Wrap the dynamic-script queue drain in try/finally so __dynScriptBusy is always
  cleared. An unexpected throw used to wedge the queue and silently block every
  later dynamic script on the page.
- Fire load/error through dispatchEvent only. dispatchEvent already invokes the
  element's onload property handler, so the extra explicit onload() call
  double-fired it. This also drops the stale enqueue-time callback snapshot and
  reads handlers live off the element.

---------

Co-authored-by: SGavrl <277843151+SGavrl@users.noreply.github.com>
2026-06-09 10:27:50 +02:00
zhangyang-crazy-oneandzhangyang-crazy-one 98b62e53e8 fix(api/js): Browser cookie jar + base href + Node.baseURI + XHR EventTarget + Page::settle (closes #241,#242,#244,#245,#247) (#243)
* fix(api): Browser::cookies() now shares context's CookieJar

Previously Browser::build() created its own empty CookieJar,
while BrowserContext internally creates a separate jar that
actually receives Set-Cookie headers from HTTP responses.

This caused browser.cookies().get_all() to always return
empty after navigation — the populated jar was unreachable.

Fix: use context.cookie_jar instead of creating a separate
one. Also removes redundant cookie-loading logic in
Browser::build() since BrowserContext::_new_inner already
handles loading from storage_dir.

Closes #241

* fix(browser): add <base href> support for script URL resolution (closes #242)

Previously resolve_base_url() returned the document URL directly,
causing relative <script src> URLs to resolve against the page URL
rather than the <base href> target. On SPAs served from sub-paths
(Angular/Vite/Next.js), this caused all bundle scripts to 404.

Add resolve_base_url() that queries DOM for <base href> and uses it
as the base for relative URL resolution per HTML spec. Applied to
both regular scripts and ES modules in execute_scripts().

* fix(js): implement Node.baseURI getter per WHATWG DOM spec (closes #244)

Node.baseURI was missing from the bootstrap.js Node class, causing
document.baseURI to return undefined. This broke Angular's
PlatformLocation.getBaseHref which calls new URL(href, document.baseURI)
-- with baseURI empty, the URL constructor throws Invalid URL.

Per https://dom.spec.whatwg.org/#dom-node-baseuri, Node.baseURI must
return the href of the first <base href> resolved against the document
URL, falling back to the document URL when no <base> is present.

* fix(js): add XMLHttpRequestEventTarget + XHR dispatchEvent (closes #245)

XHR shim lacked dispatchEvent and XMLHttpRequestEventTarget, causing
zone.js (Angular/zone.js) to crash with 'Cannot read properties of
undefined (reading call)' when Angular's HttpClient makes XHR calls.

Add spec-compliant XMLHttpRequestEventTarget base class with
addEventListener/removeEventListener/dispatchEvent. Make XHR extend
it so zone.js can walk the proto chain correctly.

* fix(js): fix XHR super() call order in ES6 class extends

* fix(js): use base href for dynamic script/module URL resolution

Angular lazy-loaded chunks were resolving against document.location.href
instead of the <base href>. This caused all dynamic import() chunks on
the Deutsche Boerse SPA to 404 (/bond/7591.xxx.js instead of /7591.xxx.js).

Fix: resolve dynamic script src against <base href> when present, falling
back to location.href per HTML spec.

* Revert "fix(js): use base href for dynamic script/module URL resolution"

This reverts commit 88af239be38f2aa11c3c978a4db2a7482f2ed6c0.

* feat(api): expose Page::settle() to pump JS event loop (closes #247)

obscura_browser::Page already had settle(max_ms) but it was unreachable
from the public API. Exposing it allows callers to resolve Promises,
fetch callbacks, and microtasks before reading results with evaluate().

---------

Co-authored-by: zhangyang-crazy-one <zhangyang.crazyone@gmail.com>
2026-06-05 20:52:39 +02:00
2e731a2ab9 feat(api): add obscura-api crate with Browser + Page + Element + CookieStore (#191)
* feat(api): add obscura-api crate with Browser + Page API

Adds a Rust library crate exposing Obscura as a programmable browser:
- Browser: build with config (proxy, stealth, UA, storage_dir)
- Page: goto, url, evaluate, content
- BrowserBuilder with fluent API
- examples/basic.rs

Part of #169

* feat(api): add Element API + CookieStore + wait_for_selector

- Element: text(), attribute(), click() via JS evaluation
- CookieStore: set/get_all/get_for_url/save_to_file/load_from_file
- Page::wait_for_selector with 100ms polling
- Page::query_selector for single element lookup
- Updated examples/basic.rs with full interaction demo

Part of #169

* feat(api): add Browser::cookies() method

* refactor(api): move to obscura crate with feature flag

- Renamed obscura-api → obscura (per maintainer feedback #169)
- API enabled by default via 'api' feature flag
- Usage: cargo add obscura (default=api) or cargo add obscura --features api
- Removed separate obscura-api crate

* chore: exclude docker workflow from fork

* fix(api): make the obscura crate build, run, and document install correctly

- examples/basic.rs and the lib.rs doctest imported `obscura_api`, but the crate
  is named `obscura`, so both failed to compile. Use the real crate name.
- wait_for_selector / query_selector read the node id with `Value::as_u64`, but
  `evaluate` returns JS numbers as f64, so it was always None and every
  wait_for_selector timed out. Accept an integer or a non-negative finite float.
- restore .github/workflows/docker.yml, which the PR removed (a fork-specific
  change that should not land upstream).
- add crates/obscura/README.md with honest install instructions: the crate is not
  on crates.io, so depend on it via git (and it builds the embedded V8 from
  source). The `cargo add obscura` form does not work yet.

Verified: workspace + example build, the lib doctest passes, the example runs end
to end (goto, wait_for_selector, text, attribute), and the integration suite is
28/28 with no regressions.

---------

Co-authored-by: zhangyang-crazy-one <zhangyang.crazyone@gmail.com>
Co-authored-by: SGavrl <277843151+SGavrl@users.noreply.github.com>
2026-06-04 18:20:51 +00:00
906dcfcde1 feat: cookie jar persistence + --storage-dir flag + fix HTTP cookie header filtering (#114)
* feat: cookie jar persistence + storage-dir + filtering fix (clean v4)

* fix: resolve build errors, add storage_dir param throughout server chain

* fix: storage_dir end-to-end, plus two test-literal field additions

  Five issues found while testing PR 114 end-to-end.

  Build fixes (two test literals out of sync with new struct fields):
  - crates/obscura-net/src/cookies.rs: CookieInfo literal in
    test_domain_matches_subdomain_without_leading_dot was missing the
    same_site and expires fields added in #170.
  - crates/obscura-cli/src/main.rs: Command::Fetch literal in
    matcher_still_uses_fetch_variant was missing the new storage_dir
    field. Without these cargo test --workspace would not compile.

  run_fetch was branching between BrowserContext::with_storage (which
  hardcodes proxy=None, stealth=false, user_agent=None) and
  with_options (which has no storage_dir slot), so --storage-dir +
  --proxy + --stealth silently dropped the proxy and stealth flags.
  Same bug SGavrl flagged on dispatch.rs in the previous round, just on
  the other code path. Switched to with_storage_full which carries all
  four. Verified the fetch now errors out against an invalid proxy.

  run_fetch --eval short-circuited the function before
  context.save_cookies() ran, so cookies set during an eval session
  were never persisted to {storage_dir}/cookies.json. Moved the save
  above the return.

  obscura serve loaded cookies on startup but never saved them
  anywhere, so a session that set cookies via Network.setCookie or
  navigation dropped them on shutdown. Subscribed to Ctrl-C once in
  put a single save_cookies() after the loop so both the Ctrl-C path
  and the channel-closed path persist cookies. Verified end-to-end.

---------

Co-authored-by: zhangyang-crazy-one <zhangyang.crazyone@gmail.com>
Co-authored-by: SGavrl <277843151+SGavrl@users.noreply.github.com>
2026-05-26 11:09:10 +02:00
zhangyang-crazy-oneandzhangyang-crazy-one 90ededfc44 fix(cdp): improve AXNode CDP spec compliance (#67)
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 <208990894+zhangyang-crazy-one@users.noreply.github.com>
2026-05-03 17:09:19 +02:00