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

+74 -1
View File
@@ -283,6 +283,23 @@ class Node {
get nodeType() { return +_dom("node_type", this._nid); }
get nodeName() { return _domParse("node_name", this._nid) || ""; }
get ownerDocument() { return globalThis.document; }
// https://dom.spec.whatwg.org/#dom-node-baseuri
get baseURI() {
try {
const doc = globalThis.document;
const docUrl = (doc && doc.URL) || "";
const baseEl = (doc && doc.querySelector) ? doc.querySelector("base[href]") : null;
if (baseEl) {
const href = baseEl.getAttribute("href");
if (href) {
return docUrl ? new URL(href, docUrl).href : href;
}
}
return docUrl;
} catch (e) {
return "";
}
}
get textContent() { return _domParse("text_content", this._nid) ?? ""; }
set textContent(v) {
const oldChildren = _domParse("child_nodes", this._nid) || [];
@@ -2584,7 +2601,42 @@ if (typeof Headers === "undefined") {
};
}
globalThis.XMLHttpRequest = class XMLHttpRequest {
// XMLHttpRequestEventTarget — spec-required ancestor for XHR EventTarget methods.
// zone.js prefers to walk XMLHttpRequestEventTarget.prototype for addEventListener/
// removeEventListener/dispatchEvent descriptors before falling back to XHR.prototype.
class XMLHttpRequestEventTarget {
addEventListener(type, handler) {
if (!this._listeners) this._listeners = {};
if (!this._listeners[type]) this._listeners[type] = [];
this._listeners[type].push(handler);
}
removeEventListener(type, handler) {
if (this._listeners && this._listeners[type]) {
this._listeners[type] = this._listeners[type].filter(h => h !== handler);
}
}
dispatchEvent(event) {
if (!event || !event.type) return false;
const ev = (typeof event === 'object') ? event : { type: event };
ev.target = ev.target || this;
ev.currentTarget = ev.currentTarget || this;
const type = ev.type;
const handlers = (this._listeners && this._listeners[type]) || [];
for (const h of handlers) { try { h.call(this, ev); } catch (e) {} }
const prop = 'on' + type;
if (typeof this[prop] === 'function') {
try { this[prop](ev); } catch (e) {}
}
return true;
}
}
globalThis.XMLHttpRequestEventTarget = XMLHttpRequestEventTarget;
_markNative(XMLHttpRequestEventTarget);
_markNative(XMLHttpRequestEventTarget.prototype.addEventListener);
_markNative(XMLHttpRequestEventTarget.prototype.removeEventListener);
_markNative(XMLHttpRequestEventTarget.prototype.dispatchEvent);
globalThis.XMLHttpRequest = class XMLHttpRequest extends XMLHttpRequestEventTarget {
static UNSENT = 0;
static OPENED = 1;
static HEADERS_RECEIVED = 2;
@@ -2593,6 +2645,7 @@ globalThis.XMLHttpRequest = class XMLHttpRequest {
UNSENT = 0; OPENED = 1; HEADERS_RECEIVED = 2; LOADING = 3; DONE = 4;
constructor() {
super();
this.readyState = 0;
this.status = 0;
this.statusText = "";
@@ -2755,6 +2808,23 @@ globalThis.XMLHttpRequest = class XMLHttpRequest {
}
}
// Per WHATWG DOM spec — required by zone.js which patches XHR via
// Object.getOwnPropertyDescriptor on XMLHttpRequestEventTarget.prototype.
dispatchEvent(event) {
if (!event || !event.type) return false;
const ev = (typeof event === 'object') ? event : { type: event };
ev.target = ev.target || this;
ev.currentTarget = ev.currentTarget || this;
const type = ev.type;
const handlers = (this._listeners && this._listeners[type]) || [];
for (const h of handlers) { try { h.call(this, ev); } catch (e) {} }
const prop = 'on' + type;
if (typeof this[prop] === 'function') {
try { this[prop](ev); } catch (e) {}
}
return true;
}
_setReadyState(state) {
this.readyState = state;
this._fireEvent('readystatechange');
@@ -2778,6 +2848,9 @@ _markNative(XMLHttpRequest.prototype.open);
_markNative(XMLHttpRequest.prototype.send);
_markNative(XMLHttpRequest.prototype.abort);
_markNative(XMLHttpRequest.prototype.setRequestHeader);
_markNative(XMLHttpRequest.prototype.addEventListener);
_markNative(XMLHttpRequest.prototype.removeEventListener);
_markNative(XMLHttpRequest.prototype.dispatchEvent);
_markNative(XMLHttpRequest.prototype.getResponseHeader);
_markNative(XMLHttpRequest.prototype.getAllResponseHeaders);