playwright_cdp/worker.rs
1//! `Worker` — web/service/shared-worker capture via the CDP `Target` domain.
2//!
3//! [`Page::on_worker`](crate::page::Page::on_worker) enables flattened auto-attach
4//! on the page session (`Target.setAutoAttach { flatten: true }`) and dispatches a
5//! [`Worker`] for every child target whose type is `worker`, `service_worker`, or
6//! `shared_worker` (surfaced via `Target.attachedToTarget`). Each worker gets its
7//! own sub-session on the same flattened connection; evaluation runs there via
8//! `Runtime.evaluate`.
9
10use crate::cdp::session::CdpSession;
11use crate::error::{Error, Result};
12use serde::de::DeserializeOwned;
13use serde_json::{json, Value};
14use std::sync::Arc;
15
16/// A web/service/shared worker owned by a page.
17///
18/// Produced by [`Page::on_worker`](crate::page::Page::on_worker). The worker is
19/// driven through its own CDP sub-session; call [`evaluate`](Worker::evaluate)
20/// to run JS in the worker's execution context.
21#[derive(Clone)]
22pub struct Worker {
23 inner: Arc<WorkerInner>,
24}
25
26struct WorkerInner {
27 url: String,
28 session: CdpSession,
29}
30
31impl Worker {
32 pub(crate) fn new(connection: Arc<crate::cdp::connection::CdpConnection>, session_id: String, url: String) -> Self {
33 let session = CdpSession::target(connection, session_id);
34 // Best-effort: enable the Runtime domain on the worker session so
35 // `Runtime.evaluate` works. We don't await this here (we're in a sync
36 // ctor); the listener task fires it after construction.
37 Self {
38 inner: Arc::new(WorkerInner { url, session }),
39 }
40 }
41
42 /// The worker's script URL (e.g. `blob:...`, `https://.../sw.js`).
43 pub fn url(&self) -> &str {
44 &self.inner.url
45 }
46
47 /// (Internal) the worker's CDP sub-session.
48 #[allow(dead_code)]
49 pub(crate) fn session(&self) -> &CdpSession {
50 &self.inner.session
51 }
52
53 /// Best-effort `Runtime.enable` on the worker session so evaluate works.
54 pub(crate) async fn enable_runtime(&self) {
55 let _ = self.inner.session.send("Runtime.enable", json!({})).await;
56 }
57
58 /// Evaluate a JS expression in the worker's execution context, returning a
59 /// typed result.
60 ///
61 /// `expression` is wrapped as `(() => { return (<expression>); })()` and
62 /// the result's `value` is parsed with serde.
63 pub async fn evaluate<R: DeserializeOwned>(&self, expression: &str) -> Result<R> {
64 let wrapped = format!("(() => {{ return ({expression}); }})()");
65 let resp = self
66 .inner
67 .session
68 .send(
69 "Runtime.evaluate",
70 json!({
71 "expression": wrapped,
72 "returnByValue": true,
73 "awaitPromise": true,
74 }),
75 )
76 .await?;
77 if let Some(exc) = resp.get("exceptionDetails") {
78 let msg = exc
79 .get("exception")
80 .and_then(|e| e.get("description"))
81 .and_then(|v| v.as_str())
82 .unwrap_or("evaluation threw");
83 return Err(Error::ProtocolError(format!("eval error: {msg}")));
84 }
85 let value = resp
86 .get("result")
87 .and_then(|r| r.get("value"))
88 .cloned()
89 .unwrap_or(Value::Null);
90 serde_json::from_value::<R>(value).map_err(Error::from)
91 }
92}