playwright_rs/protocol/browser_context/bindings.rs
1use super::{BindingCallback, BindingCallbackFuture, BrowserContext};
2use crate::error::Result;
3use crate::server::channel_owner::ChannelOwner;
4use serde_json::Value;
5use std::future::Future;
6use std::sync::Arc;
7
8/// Init scripts, exposed functions and bindings.
9impl BrowserContext {
10 /// Adds a script which would be evaluated in one of the following scenarios:
11 ///
12 /// - Whenever a page is created in the browser context or is navigated.
13 /// - Whenever a child frame is attached or navigated in any page in the browser context.
14 ///
15 /// The script is evaluated after the document was created but before any of its scripts
16 /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
17 ///
18 /// # Arguments
19 ///
20 /// * `script` - Script to be evaluated in all pages in the browser context.
21 ///
22 /// # Errors
23 ///
24 /// Returns error if:
25 /// - Context has been closed
26 /// - Communication with browser process fails
27 ///
28 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
29 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
30 pub async fn add_init_script(&self, script: &str) -> Result<()> {
31 self.channel()
32 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
33 .await
34 }
35
36 /// Exposes a Rust function to every page in this browser context as
37 /// `window[name]` in JavaScript.
38 ///
39 /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
40 /// server fires a `bindingCall` event that invokes `callback` with the
41 /// deserialized arguments. The return value of `callback` is serialized back
42 /// to JavaScript so the `await window[name](…)` expression resolves with it.
43 ///
44 /// The binding is injected into every existing page and every new page
45 /// created in this context.
46 ///
47 /// # Arguments
48 ///
49 /// * `name` – JavaScript identifier that will be available as `window[name]`.
50 /// * `callback` – Async closure called with `Vec<serde_json::Value>` (the JS
51 /// arguments) and returning `serde_json::Value` (the result).
52 ///
53 /// # Errors
54 ///
55 /// Returns error if:
56 /// - The context has been closed.
57 /// - Communication with the browser process fails.
58 ///
59 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-function>
60 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
61 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
62 where
63 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
64 Fut: Future<Output = serde_json::Value> + Send + 'static,
65 {
66 self.expose_binding_internal(name, callback).await
67 }
68
69 /// Exposes a Rust function to every page in this browser context as
70 /// `window[name]` in JavaScript.
71 ///
72 /// Currently identical to [`expose_function`](Self::expose_function):
73 /// arguments arrive as plain serialized values. Upstream Playwright's
74 /// `exposeBinding` can additionally hand the callback a source
75 /// (page/frame) descriptor, which this crate does not surface yet.
76 ///
77 /// # Arguments
78 ///
79 /// * `name` – JavaScript identifier.
80 /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
81 ///
82 /// # Errors
83 ///
84 /// Returns error if:
85 /// - The context has been closed.
86 /// - Communication with the browser process fails.
87 ///
88 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-binding>
89 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
90 pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
91 where
92 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
93 Fut: Future<Output = serde_json::Value> + Send + 'static,
94 {
95 self.expose_binding_internal(name, callback).await
96 }
97
98 /// Internal implementation shared by expose_function and expose_binding.
99 async fn expose_binding_internal<F, Fut>(&self, name: &str, callback: F) -> Result<()>
100 where
101 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
102 Fut: Future<Output = serde_json::Value> + Send + 'static,
103 {
104 // Wrap callback with type erasure
105 let callback: BindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
106 Box::pin(callback(args)) as BindingCallbackFuture
107 });
108
109 // Store the callback before sending the RPC so that a race-condition
110 // where a bindingCall arrives before we finish registering is avoided.
111 self.binding_callbacks
112 .lock()
113 .unwrap()
114 .insert(name.to_string(), callback);
115
116 // Tell the Playwright server to inject window[name] into every page.
117 //
118 // The protocol also accepts `noGlobal`, which suppresses that
119 // injection. It is deliberately not exposed: it exists to support
120 // passing functions as evaluate arguments, where the binding is
121 // called through the bindings controller rather than off `window`.
122 self.channel()
123 .send_no_result("exposeBinding", serde_json::json!({ "name": name }))
124 .await
125 }
126
127 /// Deserializes binding call arguments from Playwright's protocol format.
128 ///
129 /// The `args` field in the BindingCall initializer is a JSON array of
130 /// type-tagged values; each is parsed into plain JSON. `Page`'s
131 /// `bindingCall` dispatch shares it.
132 pub(crate) fn deserialize_binding_args(raw_args: &Value) -> Vec<Value> {
133 let Some(arr) = raw_args.as_array() else {
134 return vec![];
135 };
136
137 arr.iter()
138 .map(|arg| {
139 // Each arg is a direct Playwright type-tagged value, e.g. {"n": 3} or {"s": "hello"}
140 // (NOT wrapped in {"value": ..., "handles": []} — that format is only for evaluate args)
141 crate::protocol::evaluate_conversion::parse_value(arg, None)
142 })
143 .collect()
144 }
145}