use serde_json::json;
use zendriver_transport::{ObserverError, PausedSession, TargetObserver};
pub(crate) const FORCE_OPEN_SHADOW_ROOT_SCRIPT: &str = r#"(function () {
try {
var proto = Element.prototype;
var original = proto.attachShadow;
if (typeof original !== "function" || original.__zendriverForcedOpen) {
return;
}
var patched = function attachShadow(init) {
var opts = (init && typeof init === "object") ? init : {};
var forced = Object.assign({}, opts, { mode: "open" });
return original.call(this, forced);
};
patched.__zendriverForcedOpen = true;
Object.defineProperty(proto, "attachShadow", {
value: patched,
writable: true,
configurable: true,
});
} catch (e) {
/* never break page load if the patch can't apply */
}
})();"#;
#[derive(Debug, Default)]
pub(crate) struct ShadowRootObserver;
#[async_trait::async_trait]
impl TargetObserver for ShadowRootObserver {
fn name(&self) -> &'static str {
"force-open-shadow-roots"
}
async fn on_target_attached(&self, session: PausedSession<'_>) -> Result<(), ObserverError> {
if session.target_info.kind != "page" {
return Ok(());
}
session.call("Page.enable", json!({})).await?;
session
.call(
"Page.addScriptToEvaluateOnNewDocument",
json!({
"source": FORCE_OPEN_SHADOW_ROOT_SCRIPT,
"includeCommandLineAPI": false,
"runImmediately": true,
}),
)
.await?;
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn script_overrides_attach_shadow_to_open() {
assert!(FORCE_OPEN_SHADOW_ROOT_SCRIPT.contains("attachShadow"));
assert!(FORCE_OPEN_SHADOW_ROOT_SCRIPT.contains("\"open\""));
assert!(FORCE_OPEN_SHADOW_ROOT_SCRIPT.contains("Object.assign"));
}
#[tokio::test]
async fn observer_injects_script_on_page_attach() {
use serde_json::json;
use zendriver_transport::testing::MockConnection;
let observer = std::sync::Arc::new(ShadowRootObserver);
let (mut mock, conn) = MockConnection::pair_with_observers(vec![observer]);
mock.emit_event(
"Target.attachedToTarget",
json!({
"sessionId": "S1",
"targetInfo": {
"targetId": "T1",
"type": "page",
"url": "about:blank",
"attached": true,
},
"waitingForDebugger": true,
}),
)
.await;
for expected in [
"Page.enable",
"Page.addScriptToEvaluateOnNewDocument",
"Runtime.runIfWaitingForDebugger",
] {
let id =
tokio::time::timeout(std::time::Duration::from_secs(2), mock.expect_cmd(expected))
.await
.unwrap_or_else(|_| panic!("did not see {expected} within 2s"));
if expected == "Page.addScriptToEvaluateOnNewDocument" {
let frame = mock.last_sent();
let source = frame["params"]["source"].as_str().unwrap_or_default();
assert!(
source.contains("attachShadow") && source.contains("\"open\""),
"injected script must force attachShadow open: {source}"
);
}
mock.reply(id, json!({})).await;
}
conn.shutdown();
}
}