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