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.
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.
* 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#246Closes#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>
* 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>
* 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>
* 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>
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>