fix: SVGAnimatedString, :has() selectors, and cookie persistence on serve

This commit is contained in:
SGavrl authored and GitHub committed 2026-06-29 11:00:22 +00:00
commit 50ef1eecbd
3 files changed
+107 -7

No files matched your search

+27 -5
View File
@@ -366,11 +366,33 @@ async fn cdp_processor(
let mut deferred: std::collections::VecDeque<ServerMessage> =
std::collections::VecDeque::new();
// Subscribe to Ctrl-C once. The future is single-shot, so we break out of
// the outer loop when it fires and never poll it again. Without this the
// accept loop just exits and any cookies set during the session are lost
// before `BrowserContext::save_cookies()` runs.
let mut shutdown = Box::pin(tokio::signal::ctrl_c());
// Subscribe to graceful-shutdown signals once. The future is single-shot,
// so we break out of the outer loop when it fires and never poll it again.
// Without this the accept loop just exits and any cookies set during the
// session are lost before `BrowserContext::save_cookies()` runs. We watch
// SIGTERM as well as Ctrl-C (SIGINT) so `docker stop` / `kill`, which send
// SIGTERM, also flush cookies to the storage dir (issue #333).
let mut shutdown = Box::pin(async {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
match signal(SignalKind::terminate()) {
Ok(mut term) => {
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
Err(_) => {
let _ = tokio::signal::ctrl_c().await;
}
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
});
loop {
// Drain any deferred messages from the previous interception window
+36 -1
View File
@@ -167,6 +167,15 @@ impl<'i> parser::Parser<'i> for ObscuraSelectorParser {
type Impl = ObscuraSelector;
type Error = SelectorParseErrorKind<'i>;
// Allow `:has()`. The selectors crate gates relative-selector parsing on
// this (default false), so without it `a:has(p)` failed to parse and the
// error was swallowed into an empty match set. Matching already passes a
// SelectorCaches (which holds the relative-selector cache), so enabling
// parsing is sufficient.
fn parse_has(&self) -> bool {
true
}
fn parse_non_ts_pseudo_class(
&self,
_location: cssparser::SourceLocation,
@@ -219,7 +228,18 @@ impl<'a> Element for DomElement<'a> {
type Impl = ObscuraSelector;
fn opaque(&self) -> OpaqueElement {
OpaqueElement::new(self)
// Must be stable per node. DomElement is Copy and gets a fresh stack
// address on every traversal step, so OpaqueElement::new(self) returns a
// different id for the same node each call. That breaks `:has` anchor
// matching, which compares the anchor's opaque against the element
// reached by walking up the tree. Key off the node's stable slot in the
// tree's Vec instead (OpaqueElement only compares the address, never
// dereferences it, and the Vec is not mutated during a query).
let inner = self.tree.borrow_inner();
match inner.nodes.get(self.node_id.index()) {
Some(slot) => OpaqueElement::new(slot),
None => OpaqueElement::new(self),
}
}
fn parent_element(&self) -> Option<Self> {
@@ -666,6 +686,21 @@ mod tests {
assert_eq!(results.len(), 3);
}
#[test]
fn test_has_parses() {
// Isolate parse from match: :has must parse, not error.
assert!(super::parse_selector("a:has(p.bt)").is_ok(), ":has failed to parse");
}
#[test]
fn test_query_selector_has() {
let tree = parse_html(r#"<a><p class="bt">x</p></a>"#);
let all = tree.query_selector_all("a:has(p.bt)").unwrap();
assert_eq!(all.len(), 1, "a:has(p.bt) should match the <a>");
let none = tree.query_selector_all("a:has(span.bt)").unwrap();
assert_eq!(none.len(), 0, "a:has(span.bt) should match nothing");
}
#[test]
fn test_query_selector_descendant() {
let tree =
+44 -1
View File
@@ -959,7 +959,15 @@ class Element extends Node {
}
get id() { return this.getAttribute("id") || ""; }
set id(v) { this.setAttribute("id", v); }
get className() { return this.getAttribute("class") || ""; }
get className() {
// SVG elements reflect class as an SVGAnimatedString (.baseVal/.animVal),
// not a plain string. Anti-fraud sensors read el.className.animVal.
if (this.namespaceURI === "http://www.w3.org/2000/svg") {
if (!this._svgClassName) this._svgClassName = new SVGAnimatedString(this, "class");
return this._svgClassName;
}
return this.getAttribute("class") || "";
}
set className(v) { this.setAttribute("class", v); }
get namespaceURI() {
// createElementNS records the requested namespace on _ns; an empty string
@@ -1581,6 +1589,16 @@ class Element extends Node {
// members use. Other elements reflect the raw attribute.
get href() {
const ln = this.localName;
// SVG href-bearing elements reflect href as an SVGAnimatedString (with the
// legacy xlink:href as a fallback), not a resolved URL string. Checked
// before the HTML <a> path because an SVG <a> also has localName 'a'.
if (this.namespaceURI === "http://www.w3.org/2000/svg" &&
(ln === 'a' || ln === 'image' || ln === 'use' || ln === 'script' ||
ln === 'pattern' || ln === 'filter' || ln === 'textPath' || ln === 'mpath' ||
ln === 'linearGradient' || ln === 'radialGradient' || ln === 'feImage' || ln === 'tref')) {
if (!this._svgHref) this._svgHref = new SVGAnimatedString(this, "href", "xlink:href");
return this._svgHref;
}
if (ln === 'a' || ln === 'area') {
const raw = this.getAttribute('href');
if (raw === null) return '';
@@ -4997,6 +5015,31 @@ globalThis.HTMLLegendElement = Element;
globalThis.HTMLProgressElement = Element;
globalThis.HTMLDetailsElement = Element;
globalThis.HTMLDialogElement = Element;
// SVGAnimatedString backs the className and href reflections on SVG elements.
// baseVal and animVal both read the live attribute (no SMIL animation), and
// baseVal is writable. Used by the SVG-aware get className()/get href() above.
function SVGAnimatedString(el, attr, fallbackAttr) {
this._el = el;
this._attr = attr;
this._fallback = fallbackAttr || null;
}
SVGAnimatedString.prototype._read = function() {
let v = this._el.getAttribute(this._attr);
if (v === null && this._fallback) v = this._el.getAttribute(this._fallback);
return v == null ? '' : v;
};
Object.defineProperty(SVGAnimatedString.prototype, 'baseVal', {
get() { return this._read(); },
set(v) { this._el.setAttribute(this._attr, String(v)); },
configurable: true, enumerable: true,
});
Object.defineProperty(SVGAnimatedString.prototype, 'animVal', {
get() { return this._read(); },
configurable: true, enumerable: true,
});
Object.defineProperty(SVGAnimatedString.prototype, Symbol.toStringTag, { value: 'SVGAnimatedString', configurable: true });
_markNative(SVGAnimatedString);
globalThis.SVGElement = Element;
globalThis.SVGSVGElement = Element;
globalThis.CharacterData = CharacterData;