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 <[email protected]>
This commit is contained in:
zhangyang-crazy-oneandzhangyang-crazy-one authored and GitHub committed 2026-06-05 20:52:39 +02:00
1 parent 6dea35bd0c
commit 98b62e53e8
4 files changed
+112 -16

No files matched your search

+25 -2
View File
@@ -282,8 +282,31 @@ impl Page {
self.js = Some(rt);
}
/// Resolve the document base URL per HTML spec:
/// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
/// Falls back to self.url when no <base href> exists.
fn resolve_base_url(&self) -> Option<url::Url> {
let doc_url = self.url.as_ref()?;
let base_href: Option<String> = self.js.as_ref().and_then(|js| {
js.with_dom(|dom| {
match dom.query_selector("base[href]") {
Ok(Some(nid)) => {
dom.get_node(nid).and_then(|n| n.get_attribute("href").map(|s| s.to_string()))
}
_ => None,
}
}).flatten()
});
match base_href {
Some(href) => doc_url.join(&href).ok(),
None => Some(doc_url.clone()),
}
}
async fn execute_scripts(&mut self) {
tracing::info!("execute_scripts called, js runtime exists: {}", self.js.is_some());
// Compute document base URL, respecting <base href>.
let document_base = self.resolve_base_url();
// Soft deadline on the entire script-execution phase. Heavy SPAs
// (GitHub, Linear, CodeSandbox) ship 50+ scripts and our serial
// fetch + execute loop can blow past a 25s Puppeteer goto timeout.
@@ -397,7 +420,7 @@ impl Page {
if let Some(src_url) = &script.src {
let full_url = if src_url.starts_with("http://") || src_url.starts_with("https://") {
src_url.clone()
} else if let Some(base) = &self.url {
} else if let Some(base) = &document_base {
base.join(src_url).map(|u| u.to_string()).unwrap_or_else(|_| src_url.clone())
} else {
src_url.clone()
@@ -557,7 +580,7 @@ impl Page {
if let Some(ref src) = module_script.src {
let full_url = if src.starts_with("http://") || src.starts_with("https://") {
src.clone()
} else if let Some(base) = &self.url {
} else if let Some(base) = &document_base {
base.join(src).map(|u| u.to_string()).unwrap_or_else(|_| src.clone())
} else {
src.clone()