use std::time::Duration;
use serde::{Serialize, de::DeserializeOwned};
use tracing::{debug, instrument, trace};
use viewpoint_cdp::protocol::runtime::{
CallFunctionOnParams, EvaluateParams, EvaluateResult, ReleaseObjectParams,
};
use crate::error::PageError;
use super::Page;
mod wait;
pub use wait::{Polling, WaitForFunctionBuilder};
pub(super) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub struct JsHandle {
object_id: String,
page_session_id: String,
connection: std::sync::Arc<viewpoint_cdp::CdpConnection>,
}
impl JsHandle {
pub(crate) fn new(
object_id: String,
page_session_id: String,
connection: std::sync::Arc<viewpoint_cdp::CdpConnection>,
) -> Self {
Self {
object_id,
page_session_id,
connection,
}
}
pub fn object_id(&self) -> &str {
&self.object_id
}
pub async fn json_value<T: DeserializeOwned>(&self) -> Result<T, PageError> {
let params = CallFunctionOnParams {
function_declaration: "function() { return this; }".to_string(),
object_id: Some(self.object_id.clone()),
arguments: None,
silent: Some(false),
return_by_value: Some(true),
generate_preview: None,
user_gesture: None,
await_promise: Some(true),
execution_context_id: None,
object_group: None,
throw_on_side_effect: None,
unique_context_id: None,
serialization_options: None,
};
let result: viewpoint_cdp::protocol::runtime::CallFunctionOnResult = self
.connection
.send_command(
"Runtime.callFunctionOn",
Some(params),
Some(&self.page_session_id),
)
.await?;
if let Some(exception) = result.exception_details {
return Err(PageError::EvaluationFailed(exception.text));
}
let value = result.result.value.unwrap_or(serde_json::Value::Null);
serde_json::from_value(value)
.map_err(|e| PageError::EvaluationFailed(format!("Failed to deserialize: {e}")))
}
pub async fn dispose(self) -> Result<(), PageError> {
self.connection
.send_command::<_, serde_json::Value>(
"Runtime.releaseObject",
Some(ReleaseObjectParams {
object_id: self.object_id,
}),
Some(&self.page_session_id),
)
.await?;
Ok(())
}
}
impl Page {
pub(crate) async fn evaluate_js_raw(
&self,
expression: &str,
) -> Result<serde_json::Value, PageError> {
if self.closed {
return Err(PageError::Closed);
}
let params = EvaluateParams {
expression: expression.to_string(),
object_group: None,
include_command_line_api: None,
silent: Some(true),
context_id: None,
return_by_value: Some(true),
await_promise: Some(false),
};
let result: EvaluateResult = self
.connection
.send_command("Runtime.evaluate", Some(params), Some(&self.session_id))
.await?;
if let Some(exception) = result.exception_details {
return Err(PageError::EvaluationFailed(exception.text));
}
result
.result
.value
.ok_or_else(|| PageError::EvaluationFailed("No result value".to_string()))
}
#[instrument(level = "debug", skip(self), fields(expression = %expression))]
pub async fn evaluate<T: DeserializeOwned>(&self, expression: &str) -> Result<T, PageError> {
self.evaluate_internal(expression, None, DEFAULT_TIMEOUT)
.await
}
#[instrument(level = "debug", skip(self, arg), fields(expression = %expression))]
pub async fn evaluate_with_arg<T: DeserializeOwned, A: Serialize>(
&self,
expression: &str,
arg: A,
) -> Result<T, PageError> {
let arg_json = serde_json::to_value(arg).map_err(|e| {
PageError::EvaluationFailed(format!("Failed to serialize argument: {e}"))
})?;
self.evaluate_internal(expression, Some(arg_json), DEFAULT_TIMEOUT)
.await
}
#[instrument(level = "debug", skip(self), fields(expression = %expression))]
pub async fn evaluate_handle(&self, expression: &str) -> Result<JsHandle, PageError> {
if self.closed {
return Err(PageError::Closed);
}
debug!("Evaluating expression for handle");
let wrapped = wrap_expression(expression);
let params = EvaluateParams {
expression: wrapped,
object_group: Some("viewpoint".to_string()),
include_command_line_api: None,
silent: Some(false),
context_id: None,
return_by_value: Some(false), await_promise: Some(true),
};
let result: EvaluateResult = self
.connection
.send_command("Runtime.evaluate", Some(params), Some(&self.session_id))
.await?;
if let Some(exception) = result.exception_details {
return Err(PageError::EvaluationFailed(exception.text));
}
let object_id = result
.result
.object_id
.ok_or_else(|| PageError::EvaluationFailed("Result is not an object".to_string()))?;
Ok(JsHandle::new(
object_id,
self.session_id.clone(),
self.connection.clone(),
))
}
async fn evaluate_internal<T: DeserializeOwned>(
&self,
expression: &str,
arg: Option<serde_json::Value>,
_timeout: Duration,
) -> Result<T, PageError> {
if self.closed {
return Err(PageError::Closed);
}
trace!(expression = expression, "Evaluating JavaScript");
let final_expression = if let Some(arg_value) = arg {
let arg_json = serde_json::to_string(&arg_value)
.map_err(|e| PageError::EvaluationFailed(format!("Failed to serialize: {e}")))?;
format!("({expression})({arg_json})")
} else {
wrap_expression(expression)
};
let params = EvaluateParams {
expression: final_expression,
object_group: None,
include_command_line_api: None,
silent: Some(false),
context_id: None,
return_by_value: Some(true),
await_promise: Some(true),
};
let result: EvaluateResult = self
.connection
.send_command("Runtime.evaluate", Some(params), Some(&self.session_id))
.await?;
if let Some(exception) = result.exception_details {
return Err(PageError::EvaluationFailed(exception.text));
}
let value = result.result.value.unwrap_or(serde_json::Value::Null);
serde_json::from_value(value)
.map_err(|e| PageError::EvaluationFailed(format!("Failed to deserialize: {e}")))
}
}
pub(super) fn wrap_expression(expression: &str) -> String {
let trimmed = expression.trim();
if trimmed.starts_with("()")
|| trimmed.starts_with("async ()")
|| trimmed.starts_with("async()")
|| trimmed.starts_with("function")
|| trimmed.starts_with("async function")
{
format!("({trimmed})()")
} else {
trimmed.to_string()
}
}