1use serde::Deserialize;
9use serde_json::{Map, Value};
10
11#[derive(Debug, Clone)]
15pub struct RawEvent {
16 raw: Map<String, Value>,
17}
18
19impl RawEvent {
20 pub(crate) fn new(raw: Map<String, Value>) -> Self {
21 Self { raw }
22 }
23
24 pub fn kind(&self) -> Option<&str> {
31 self.raw.get("type").and_then(|v| v.as_str())
32 }
33
34 pub fn as_value(&self) -> &Map<String, Value> {
36 &self.raw
37 }
38
39 pub fn into_value(self) -> Value {
41 Value::Object(self.raw)
42 }
43
44 pub fn into_typed(self) -> TraceEvent {
49 let value = Value::Object(self.raw);
55 match TypedEnum::deserialize(&value) {
56 Ok(t) => t.into(),
57 Err(_) => match value {
58 Value::Object(raw) => TraceEvent::Unknown(RawEvent { raw }),
59 _ => unreachable!("the value was built from an object above"),
60 },
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
69pub enum TraceEvent {
70 ContextOptions(ContextOptions),
71 Before(BeforeEvent),
72 Input(InputEvent),
73 Log(LogEvent),
74 After(AfterEvent),
75 Console(ConsoleEvent),
76 Event(SystemEvent),
77 FrameSnapshot(FrameSnapshotEvent),
78 ScreencastFrame(ScreencastFrameEvent),
79 Unknown(RawEvent),
82}
83
84#[derive(Deserialize)]
87#[serde(tag = "type", rename_all = "kebab-case")]
88enum TypedEnum {
89 ContextOptions(ContextOptions),
90 Before(BeforeEvent),
91 Input(InputEvent),
92 Log(LogEvent),
93 After(AfterEvent),
94 Console(ConsoleEvent),
95 Event(SystemEvent),
96 FrameSnapshot {
98 snapshot: FrameSnapshotWire,
99 },
100 ScreencastFrame(ScreencastFrameEvent),
101}
102
103impl From<TypedEnum> for TraceEvent {
104 fn from(t: TypedEnum) -> Self {
105 match t {
106 TypedEnum::ContextOptions(c) => TraceEvent::ContextOptions(c),
107 TypedEnum::Before(b) => TraceEvent::Before(b),
108 TypedEnum::Input(i) => TraceEvent::Input(i),
109 TypedEnum::Log(l) => TraceEvent::Log(l),
110 TypedEnum::After(a) => TraceEvent::After(a),
111 TypedEnum::Console(c) => TraceEvent::Console(c),
112 TypedEnum::Event(e) => TraceEvent::Event(e),
113 TypedEnum::FrameSnapshot { snapshot } => TraceEvent::FrameSnapshot(snapshot.into()),
114 TypedEnum::ScreencastFrame(s) => TraceEvent::ScreencastFrame(s),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct ContextOptions {
124 pub version: u32,
125 #[serde(default)]
126 pub browser_name: String,
127 #[serde(default)]
128 pub playwright_version: String,
129 #[serde(default)]
130 pub platform: String,
131 #[serde(default)]
132 pub sdk_language: String,
133 #[serde(default)]
134 pub test_id_attribute_name: String,
135 #[serde(default)]
136 pub wall_time: f64,
137 #[serde(default)]
138 pub monotonic_time: f64,
139 #[serde(default)]
140 pub context_id: String,
141 #[serde(default)]
144 pub options: Value,
145}
146
147#[derive(Debug, Clone, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct BeforeEvent {
152 #[serde(deserialize_with = "call_id")]
153 pub call_id: String,
154 pub start_time: f64,
155 #[serde(default)]
156 pub class: String,
157 #[serde(default)]
158 pub method: String,
159 #[serde(default)]
160 pub params: Value,
161 #[serde(default)]
162 pub title: Option<String>,
163 pub page_id: Option<String>,
164 pub before_snapshot: Option<String>,
165 pub step_id: Option<String>,
168 #[serde(default, deserialize_with = "optional_call_id")]
169 pub parent_id: Option<String>,
170}
171
172#[derive(Debug, Clone, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct InputEvent {
177 #[serde(deserialize_with = "call_id")]
178 pub call_id: String,
179 pub point: Option<Point>,
180 pub input_snapshot: Option<String>,
181}
182
183#[derive(Debug, Clone, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct LogEvent {
187 #[serde(deserialize_with = "call_id")]
188 pub call_id: String,
189 pub message: String,
190 pub time: f64,
191}
192
193#[derive(Debug, Clone, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct AfterEvent {
197 #[serde(deserialize_with = "call_id")]
198 pub call_id: String,
199 pub end_time: f64,
200 #[serde(default)]
201 pub result: Option<Value>,
202 #[serde(default)]
203 pub error: Option<ActionError>,
204 pub after_snapshot: Option<String>,
205 pub point: Option<Point>,
206}
207
208#[derive(Debug, Clone, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct ConsoleEvent {
212 #[serde(rename = "messageType", default)]
219 pub level: String,
220 #[serde(default)]
221 pub text: String,
222 #[serde(default)]
223 pub args: Vec<Value>,
224 pub location: Option<ConsoleLocation>,
225 pub time: f64,
226 pub page_id: Option<String>,
227}
228
229#[derive(Debug, Clone, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct ConsoleLocation {
232 #[serde(default)]
233 pub url: String,
234 #[serde(default)]
235 pub line_number: u32,
236 #[serde(default)]
237 pub column_number: u32,
238}
239
240#[derive(Debug, Clone, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct SystemEvent {
245 #[serde(default)]
246 pub class: String,
247 #[serde(default)]
248 pub method: String,
249 #[serde(default)]
250 pub params: Value,
251 pub time: f64,
252 pub page_id: Option<String>,
253}
254
255#[derive(Debug, Clone)]
264pub struct FrameSnapshotEvent {
265 pub call_id: String,
266 pub phase: ActionPhase,
268 pub snapshot_name: Option<String>,
271 pub page_id: String,
272 pub frame_id: String,
273 pub frame_url: String,
274 pub doctype: String,
275 pub html: Value,
277 pub viewport: Option<Viewport>,
278 pub timestamp: f64,
279 pub wall_time: f64,
280 pub collection_time: f64,
281 pub is_main_frame: bool,
282 pub resource_overrides: Vec<ResourceOverride>,
283}
284
285#[derive(Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct FrameSnapshotWire {
289 #[serde(deserialize_with = "call_id")]
290 call_id: String,
291 #[serde(default)]
292 phase: Option<ActionPhase>,
293 #[serde(default)]
294 snapshot_name: Option<String>,
295 page_id: String,
296 frame_id: String,
297 #[serde(default)]
298 frame_url: String,
299 #[serde(default)]
300 doctype: String,
301 #[serde(default)]
302 html: Value,
303 viewport: Option<Viewport>,
304 timestamp: f64,
305 #[serde(default)]
306 wall_time: f64,
307 #[serde(default)]
308 collection_time: f64,
309 #[serde(default)]
310 is_main_frame: bool,
311 #[serde(default)]
312 resource_overrides: Vec<ResourceOverride>,
313}
314
315impl From<FrameSnapshotWire> for FrameSnapshotEvent {
316 fn from(wire: FrameSnapshotWire) -> Self {
317 let phase = wire
318 .phase
319 .or_else(|| wire.snapshot_name.as_deref().map(ActionPhase::from_v8_name))
320 .unwrap_or(ActionPhase::Other);
321 Self {
322 call_id: wire.call_id,
323 phase,
324 snapshot_name: wire.snapshot_name,
325 page_id: wire.page_id,
326 frame_id: wire.frame_id,
327 frame_url: wire.frame_url,
328 doctype: wire.doctype,
329 html: wire.html,
330 viewport: wire.viewport,
331 timestamp: wire.timestamp,
332 wall_time: wire.wall_time,
333 collection_time: wire.collection_time,
334 is_main_frame: wire.is_main_frame,
335 resource_overrides: wire.resource_overrides,
336 }
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
342#[serde(rename_all = "lowercase")]
343pub enum ActionPhase {
344 Before,
346 Action,
348 After,
350 #[serde(other)]
352 Other,
353}
354
355impl ActionPhase {
356 fn from_v8_name(name: &str) -> Self {
359 match name.split('@').next() {
360 Some("before") => Self::Before,
361 Some("input") => Self::Action,
362 Some("after") => Self::After,
363 _ => Self::Other,
364 }
365 }
366}
367
368#[derive(Debug, Clone, Copy, Deserialize)]
369pub struct Viewport {
370 pub width: u32,
371 pub height: u32,
372}
373
374#[derive(Debug, Clone, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct ResourceOverride {
379 pub url: String,
380 #[serde(default, alias = "sha1", deserialize_with = "resource_path")]
384 pub file: Option<String>,
385 #[serde(rename = "ref", default)]
388 pub reference: Option<u64>,
389}
390
391#[derive(Debug, Clone, Deserialize)]
393#[serde(rename_all = "camelCase")]
394pub struct ScreencastFrameEvent {
395 pub page_id: String,
396 #[serde(alias = "sha1", deserialize_with = "required_resource_path")]
400 pub file: String,
401 pub width: u32,
402 pub height: u32,
403 pub timestamp: f64,
404}
405
406const RESOURCES_PREFIX: &str = "resources/";
410
411fn to_resource_path(mut raw: String) -> String {
414 if !raw.contains('/') {
415 raw.insert_str(0, RESOURCES_PREFIX);
416 }
417 raw
418}
419
420pub(crate) fn resource_path<'de, D>(
422 deserializer: D,
423) -> std::result::Result<Option<String>, D::Error>
424where
425 D: serde::Deserializer<'de>,
426{
427 Ok(Option::<String>::deserialize(deserializer)?.map(to_resource_path))
428}
429
430pub(crate) fn required_resource_path<'de, D>(
432 deserializer: D,
433) -> std::result::Result<String, D::Error>
434where
435 D: serde::Deserializer<'de>,
436{
437 Ok(to_resource_path(String::deserialize(deserializer)?))
438}
439
440#[derive(Deserialize)]
445#[serde(untagged)]
446enum CallIdWire {
447 Text(String),
448 Number(u64),
449}
450
451impl From<CallIdWire> for String {
452 fn from(id: CallIdWire) -> Self {
453 match id {
454 CallIdWire::Text(id) => id,
455 CallIdWire::Number(id) => id.to_string(),
456 }
457 }
458}
459
460pub(crate) fn call_id<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
462where
463 D: serde::Deserializer<'de>,
464{
465 Ok(CallIdWire::deserialize(deserializer)?.into())
466}
467
468pub(crate) fn optional_call_id<'de, D>(
470 deserializer: D,
471) -> std::result::Result<Option<String>, D::Error>
472where
473 D: serde::Deserializer<'de>,
474{
475 Ok(Option::<CallIdWire>::deserialize(deserializer)?.map(String::from))
476}
477
478#[derive(Debug, Clone, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct ActionError {
482 #[serde(default)]
483 pub name: String,
484 #[serde(default)]
485 pub message: String,
486}
487
488#[derive(Debug, Clone, Copy, Deserialize)]
491pub struct Point {
492 pub x: f64,
493 pub y: f64,
494}