use serde::Deserialize;
use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct RawEvent {
raw: Map<String, Value>,
}
impl RawEvent {
pub(crate) fn new(raw: Map<String, Value>) -> Self {
Self { raw }
}
pub fn kind(&self) -> Option<&str> {
self.raw.get("type").and_then(|v| v.as_str())
}
pub fn as_value(&self) -> &Map<String, Value> {
&self.raw
}
pub fn into_value(self) -> Value {
Value::Object(self.raw)
}
pub fn into_typed(self) -> TraceEvent {
let value = Value::Object(self.raw);
match TypedEnum::deserialize(&value) {
Ok(t) => t.into(),
Err(_) => match value {
Value::Object(raw) => TraceEvent::Unknown(RawEvent { raw }),
_ => unreachable!("the value was built from an object above"),
},
}
}
}
#[derive(Debug, Clone)]
pub enum TraceEvent {
ContextOptions(ContextOptions),
Before(BeforeEvent),
Input(InputEvent),
Log(LogEvent),
After(AfterEvent),
Console(ConsoleEvent),
Event(SystemEvent),
FrameSnapshot(FrameSnapshotEvent),
ScreencastFrame(ScreencastFrameEvent),
Unknown(RawEvent),
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
enum TypedEnum {
ContextOptions(ContextOptions),
Before(BeforeEvent),
Input(InputEvent),
Log(LogEvent),
After(AfterEvent),
Console(ConsoleEvent),
Event(SystemEvent),
FrameSnapshot {
snapshot: FrameSnapshotWire,
},
ScreencastFrame(ScreencastFrameEvent),
}
impl From<TypedEnum> for TraceEvent {
fn from(t: TypedEnum) -> Self {
match t {
TypedEnum::ContextOptions(c) => TraceEvent::ContextOptions(c),
TypedEnum::Before(b) => TraceEvent::Before(b),
TypedEnum::Input(i) => TraceEvent::Input(i),
TypedEnum::Log(l) => TraceEvent::Log(l),
TypedEnum::After(a) => TraceEvent::After(a),
TypedEnum::Console(c) => TraceEvent::Console(c),
TypedEnum::Event(e) => TraceEvent::Event(e),
TypedEnum::FrameSnapshot { snapshot } => TraceEvent::FrameSnapshot(snapshot.into()),
TypedEnum::ScreencastFrame(s) => TraceEvent::ScreencastFrame(s),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContextOptions {
pub version: u32,
#[serde(default)]
pub browser_name: String,
#[serde(default)]
pub playwright_version: String,
#[serde(default)]
pub platform: String,
#[serde(default)]
pub sdk_language: String,
#[serde(default)]
pub test_id_attribute_name: String,
#[serde(default)]
pub wall_time: f64,
#[serde(default)]
pub monotonic_time: f64,
#[serde(default)]
pub context_id: String,
#[serde(default)]
pub options: Value,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BeforeEvent {
pub call_id: String,
pub start_time: f64,
#[serde(default)]
pub class: String,
#[serde(default)]
pub method: String,
#[serde(default)]
pub params: Value,
#[serde(default)]
pub title: Option<String>,
pub page_id: Option<String>,
pub before_snapshot: Option<String>,
pub step_id: Option<String>,
pub parent_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InputEvent {
pub call_id: String,
pub point: Option<Point>,
pub input_snapshot: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogEvent {
pub call_id: String,
pub message: String,
pub time: f64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AfterEvent {
pub call_id: String,
pub end_time: f64,
#[serde(default)]
pub result: Option<Value>,
#[serde(default)]
pub error: Option<ActionError>,
pub after_snapshot: Option<String>,
pub point: Option<Point>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConsoleEvent {
#[serde(rename = "messageType", default)]
pub level: String,
#[serde(default)]
pub text: String,
#[serde(default)]
pub args: Vec<Value>,
pub location: Option<ConsoleLocation>,
pub time: f64,
pub page_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConsoleLocation {
#[serde(default)]
pub url: String,
#[serde(default)]
pub line_number: u32,
#[serde(default)]
pub column_number: u32,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemEvent {
#[serde(default)]
pub class: String,
#[serde(default)]
pub method: String,
#[serde(default)]
pub params: Value,
pub time: f64,
pub page_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FrameSnapshotEvent {
pub call_id: String,
pub phase: ActionPhase,
pub snapshot_name: Option<String>,
pub page_id: String,
pub frame_id: String,
pub frame_url: String,
pub doctype: String,
pub html: Value,
pub viewport: Option<Viewport>,
pub timestamp: f64,
pub wall_time: f64,
pub collection_time: f64,
pub is_main_frame: bool,
pub resource_overrides: Vec<ResourceOverride>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrameSnapshotWire {
call_id: String,
#[serde(default)]
phase: Option<ActionPhase>,
#[serde(default)]
snapshot_name: Option<String>,
page_id: String,
frame_id: String,
#[serde(default)]
frame_url: String,
#[serde(default)]
doctype: String,
#[serde(default)]
html: Value,
viewport: Option<Viewport>,
timestamp: f64,
#[serde(default)]
wall_time: f64,
#[serde(default)]
collection_time: f64,
#[serde(default)]
is_main_frame: bool,
#[serde(default)]
resource_overrides: Vec<ResourceOverride>,
}
impl From<FrameSnapshotWire> for FrameSnapshotEvent {
fn from(wire: FrameSnapshotWire) -> Self {
let phase = wire
.phase
.or_else(|| wire.snapshot_name.as_deref().map(ActionPhase::from_v8_name))
.unwrap_or(ActionPhase::Other);
Self {
call_id: wire.call_id,
phase,
snapshot_name: wire.snapshot_name,
page_id: wire.page_id,
frame_id: wire.frame_id,
frame_url: wire.frame_url,
doctype: wire.doctype,
html: wire.html,
viewport: wire.viewport,
timestamp: wire.timestamp,
wall_time: wire.wall_time,
collection_time: wire.collection_time,
is_main_frame: wire.is_main_frame,
resource_overrides: wire.resource_overrides,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActionPhase {
Before,
Action,
After,
#[serde(other)]
Other,
}
impl ActionPhase {
fn from_v8_name(name: &str) -> Self {
match name.split('@').next() {
Some("before") => Self::Before,
Some("input") => Self::Action,
Some("after") => Self::After,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Viewport {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceOverride {
pub url: String,
#[serde(default, alias = "sha1", deserialize_with = "resource_path")]
pub file: Option<String>,
#[serde(rename = "ref", default)]
pub reference: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScreencastFrameEvent {
pub page_id: String,
#[serde(alias = "sha1", deserialize_with = "required_resource_path")]
pub file: String,
pub width: u32,
pub height: u32,
pub timestamp: f64,
}
const RESOURCES_PREFIX: &str = "resources/";
fn to_resource_path(mut raw: String) -> String {
if !raw.contains('/') {
raw.insert_str(0, RESOURCES_PREFIX);
}
raw
}
pub(crate) fn resource_path<'de, D>(
deserializer: D,
) -> std::result::Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Option::<String>::deserialize(deserializer)?.map(to_resource_path))
}
pub(crate) fn required_resource_path<'de, D>(
deserializer: D,
) -> std::result::Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(to_resource_path(String::deserialize(deserializer)?))
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionError {
#[serde(default)]
pub name: String,
#[serde(default)]
pub message: String,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Point {
pub x: f64,
pub y: f64,
}