use crate::error::Result;
use crate::protocol::har_options::StartHarOptions;
use crate::server::channel::Channel;
use crate::server::channel_owner::{
ChannelOwner, ChannelOwnerImpl, DisposeReason, ParentOrConnection,
};
use crate::server::connection::ConnectionLike;
use serde::Serialize;
use serde_json::Value;
use std::any::Any;
use std::sync::Arc;
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TracingStartOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "screencast", skip_serializing_if = "Option::is_none")]
pub screenshots: Option<bool>,
#[serde(flatten)]
pub snapshots: Option<TraceSnapshots>,
#[serde(skip_serializing_if = "Option::is_none")]
pub live: Option<bool>,
}
impl TracingStartOptions {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn screenshots(mut self, screenshots: bool) -> Self {
self.screenshots = Some(screenshots);
self
}
pub fn snapshots(mut self, snapshots: impl Into<TraceSnapshots>) -> Self {
self.snapshots = Some(snapshots.into());
self
}
pub fn live(mut self, live: bool) -> Self {
self.live = Some(live);
self
}
}
impl TracingStartOptions {
pub(crate) fn to_json(&self) -> Value {
serde_json::to_value(self).expect("TracingStartOptions serialization cannot fail")
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct TraceSnapshots {
#[serde(rename = "snapshotDom", skip_serializing_if = "Option::is_none")]
pub dom: Option<bool>,
#[serde(rename = "snapshotAria", skip_serializing_if = "Option::is_none")]
pub aria: Option<bool>,
#[serde(rename = "snapshotScreen", skip_serializing_if = "Option::is_none")]
pub screen: Option<bool>,
}
impl TraceSnapshots {
pub fn dom(mut self, dom: bool) -> Self {
self.dom = Some(dom);
self
}
pub fn aria(mut self, aria: bool) -> Self {
self.aria = Some(aria);
self
}
pub fn screen(mut self, screen: bool) -> Self {
self.screen = Some(screen);
self
}
}
impl From<bool> for TraceSnapshots {
fn from(dom: bool) -> Self {
Self::default().dom(dom)
}
}
#[cfg(test)]
mod to_json_tests {
use super::*;
#[test]
fn keys_are_the_driver_spellings() {
let params = TracingStartOptions::default()
.name("t")
.screenshots(true)
.snapshots(TraceSnapshots::default().dom(true).aria(false).screen(true))
.live(false)
.to_json();
assert_eq!(
params,
serde_json::json!({
"name": "t",
"screencast": true,
"snapshotDom": true,
"snapshotAria": false,
"snapshotScreen": true,
"live": false,
})
);
}
#[test]
fn a_bool_selects_the_dom_snapshot_alone() {
let params = TracingStartOptions::default().snapshots(true).to_json();
assert_eq!(params, serde_json::json!({ "snapshotDom": true }));
}
#[test]
fn unset_options_send_no_keys() {
assert_eq!(
TracingStartOptions::default().to_json(),
serde_json::json!({})
);
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct TracingStopOptions {
pub path: Option<String>,
}
impl TracingStopOptions {
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
}
struct HarRecording {
har_id: Option<String>,
path: String,
resources_dir: Option<String>,
}
#[derive(Clone)]
pub struct Tracing {
base: ChannelOwnerImpl,
har: Arc<parking_lot::Mutex<Option<HarRecording>>>,
}
impl Tracing {
pub fn new(
parent: ParentOrConnection,
type_name: String,
guid: Arc<str>,
initializer: Value,
) -> Result<Self> {
Ok(Self {
base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
har: Arc::new(parking_lot::Mutex::new(None)),
})
}
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
pub async fn start(&self, options: impl Into<Option<TracingStartOptions>>) -> Result<()> {
let options = options.into();
let opts = options.unwrap_or_default();
self.channel()
.send_no_result("tracingStart", opts.to_json())
.await?;
let mut chunk_params = serde_json::json!({});
if let Some(name) = opts.name {
chunk_params["name"] = serde_json::Value::String(name);
}
self.channel()
.send_no_result("tracingStartChunk", chunk_params)
.await
}
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
pub async fn stop(&self, options: impl Into<Option<TracingStopOptions>>) -> Result<()> {
let options = options.into();
let path = options.and_then(|o| o.path);
let mode = if path.is_some() { "archive" } else { "discard" };
let stop_chunk_params = serde_json::json!({ "mode": mode });
let chunk_result: Value = self
.channel()
.send("tracingStopChunk", stop_chunk_params)
.await?;
self.channel()
.send_no_result("tracingStop", serde_json::json!({}))
.await?;
if let Some(dest_path) = path
&& let Some(artifact_guid) = chunk_result
.get("artifact")
.and_then(|a| a.get("guid"))
.and_then(|g| g.as_str())
{
self.save_artifact(artifact_guid, &dest_path).await?;
}
Ok(())
}
async fn save_artifact(&self, artifact_guid: &str, dest_path: &str) -> Result<()> {
use crate::protocol::artifact::Artifact;
use crate::server::connection::ConnectionExt;
let artifact = self
.connection()
.get_typed::<Artifact>(artifact_guid)
.await?;
artifact.save_as(dest_path).await
}
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
pub async fn start_har(
&self,
path: impl Into<String>,
options: impl Into<Option<StartHarOptions>>,
) -> Result<()> {
let options = options.into();
let path = path.into();
let opts = options.unwrap_or_default();
let rec_options = opts.to_record_har_json(&path);
let result: Value = self
.channel()
.send("harStart", serde_json::json!({ "options": rec_options }))
.await?;
let har_id = result
.get("harId")
.and_then(|v| v.as_str())
.map(str::to_owned);
*self.har.lock() = Some(HarRecording {
har_id,
path,
resources_dir: opts.resources_dir,
});
Ok(())
}
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
pub async fn stop_har(&self) -> Result<()> {
let Some(recording) = self.har.lock().take() else {
return Err(crate::error::Error::InvalidArgument(
"stop_har called without a matching start_har".to_string(),
));
};
let mut params = serde_json::json!({ "mode": "archive" });
if let Some(id) = &recording.har_id {
params["harId"] = Value::String(id.clone());
}
let result: Value = self.channel().send("harExport", params).await?;
let Some(artifact_guid) = result
.get("artifact")
.and_then(|a| a.get("guid"))
.and_then(|g| g.as_str())
else {
return Ok(());
};
if recording.path.ends_with(".zip") {
self.save_artifact(artifact_guid, &recording.path).await?;
} else {
let tmp_zip = format!("{}.tmp.zip", recording.path);
self.save_artifact(artifact_guid, &tmp_zip).await?;
let local_utils = self.find_local_utils()?;
local_utils
.har_unzip(
&tmp_zip,
&recording.path,
recording.resources_dir.as_deref(),
)
.await?;
let _ = std::fs::remove_file(&tmp_zip);
}
Ok(())
}
fn find_local_utils(&self) -> Result<crate::protocol::LocalUtils> {
let connection = self.connection();
connection
.all_objects_sync()
.into_iter()
.find(|o| o.type_name() == "LocalUtils")
.and_then(|o| {
o.as_any()
.downcast_ref::<crate::protocol::LocalUtils>()
.cloned()
})
.ok_or_else(|| {
crate::error::Error::ProtocolError(
"stop_har: LocalUtils not found in connection registry".to_string(),
)
})
}
}
impl ChannelOwner for Tracing {
fn guid(&self) -> &str {
self.base.guid()
}
fn type_name(&self) -> &str {
self.base.type_name()
}
fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
self.base.parent()
}
fn connection(&self) -> Arc<dyn ConnectionLike> {
self.base.connection()
}
fn initializer(&self) -> &Value {
self.base.initializer()
}
fn channel(&self) -> &Channel {
self.base.channel()
}
fn dispose(&self, reason: DisposeReason) {
self.base.dispose(reason)
}
fn adopt(&self, child: Arc<dyn ChannelOwner>) {
self.base.adopt(child)
}
fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
self.base.add_child(guid, child)
}
fn remove_child(&self, guid: &str) {
self.base.remove_child(guid)
}
fn on_event(&self, method: &str, params: Value) {
self.base.on_event(method, params)
}
fn was_collected(&self) -> bool {
self.base.was_collected()
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl std::fmt::Debug for Tracing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tracing")
.field("guid", &self.guid())
.finish()
}
}