Skip to main content

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}