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 pub call_id: String,
153 pub start_time: f64,
154 #[serde(default)]
155 pub class: String,
156 #[serde(default)]
157 pub method: String,
158 #[serde(default)]
159 pub params: Value,
160 #[serde(default)]
161 pub title: Option<String>,
162 pub page_id: Option<String>,
163 pub before_snapshot: Option<String>,
164 pub step_id: Option<String>,
165 pub parent_id: Option<String>,
166}
167
168#[derive(Debug, Clone, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct InputEvent {
173 pub call_id: String,
174 pub point: Option<Point>,
175 pub input_snapshot: Option<String>,
176}
177
178#[derive(Debug, Clone, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct LogEvent {
182 pub call_id: String,
183 pub message: String,
184 pub time: f64,
185}
186
187#[derive(Debug, Clone, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct AfterEvent {
191 pub call_id: String,
192 pub end_time: f64,
193 #[serde(default)]
194 pub result: Option<Value>,
195 #[serde(default)]
196 pub error: Option<ActionError>,
197 pub after_snapshot: Option<String>,
198 pub point: Option<Point>,
199}
200
201#[derive(Debug, Clone, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct ConsoleEvent {
205 #[serde(rename = "messageType", default)]
212 pub level: String,
213 #[serde(default)]
214 pub text: String,
215 #[serde(default)]
216 pub args: Vec<Value>,
217 pub location: Option<ConsoleLocation>,
218 pub time: f64,
219 pub page_id: Option<String>,
220}
221
222#[derive(Debug, Clone, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct ConsoleLocation {
225 #[serde(default)]
226 pub url: String,
227 #[serde(default)]
228 pub line_number: u32,
229 #[serde(default)]
230 pub column_number: u32,
231}
232
233#[derive(Debug, Clone, Deserialize)]
236#[serde(rename_all = "camelCase")]
237pub struct SystemEvent {
238 #[serde(default)]
239 pub class: String,
240 #[serde(default)]
241 pub method: String,
242 #[serde(default)]
243 pub params: Value,
244 pub time: f64,
245 pub page_id: Option<String>,
246}
247
248#[derive(Debug, Clone)]
257pub struct FrameSnapshotEvent {
258 pub call_id: String,
259 pub phase: ActionPhase,
261 pub snapshot_name: Option<String>,
264 pub page_id: String,
265 pub frame_id: String,
266 pub frame_url: String,
267 pub doctype: String,
268 pub html: Value,
270 pub viewport: Option<Viewport>,
271 pub timestamp: f64,
272 pub wall_time: f64,
273 pub collection_time: f64,
274 pub is_main_frame: bool,
275 pub resource_overrides: Vec<ResourceOverride>,
276}
277
278#[derive(Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct FrameSnapshotWire {
282 call_id: String,
283 #[serde(default)]
284 phase: Option<ActionPhase>,
285 #[serde(default)]
286 snapshot_name: Option<String>,
287 page_id: String,
288 frame_id: String,
289 #[serde(default)]
290 frame_url: String,
291 #[serde(default)]
292 doctype: String,
293 #[serde(default)]
294 html: Value,
295 viewport: Option<Viewport>,
296 timestamp: f64,
297 #[serde(default)]
298 wall_time: f64,
299 #[serde(default)]
300 collection_time: f64,
301 #[serde(default)]
302 is_main_frame: bool,
303 #[serde(default)]
304 resource_overrides: Vec<ResourceOverride>,
305}
306
307impl From<FrameSnapshotWire> for FrameSnapshotEvent {
308 fn from(wire: FrameSnapshotWire) -> Self {
309 let phase = wire
310 .phase
311 .or_else(|| wire.snapshot_name.as_deref().map(ActionPhase::from_v8_name))
312 .unwrap_or(ActionPhase::Other);
313 Self {
314 call_id: wire.call_id,
315 phase,
316 snapshot_name: wire.snapshot_name,
317 page_id: wire.page_id,
318 frame_id: wire.frame_id,
319 frame_url: wire.frame_url,
320 doctype: wire.doctype,
321 html: wire.html,
322 viewport: wire.viewport,
323 timestamp: wire.timestamp,
324 wall_time: wire.wall_time,
325 collection_time: wire.collection_time,
326 is_main_frame: wire.is_main_frame,
327 resource_overrides: wire.resource_overrides,
328 }
329 }
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
334#[serde(rename_all = "lowercase")]
335pub enum ActionPhase {
336 Before,
338 Action,
340 After,
342 #[serde(other)]
344 Other,
345}
346
347impl ActionPhase {
348 fn from_v8_name(name: &str) -> Self {
351 match name.split('@').next() {
352 Some("before") => Self::Before,
353 Some("input") => Self::Action,
354 Some("after") => Self::After,
355 _ => Self::Other,
356 }
357 }
358}
359
360#[derive(Debug, Clone, Copy, Deserialize)]
361pub struct Viewport {
362 pub width: u32,
363 pub height: u32,
364}
365
366#[derive(Debug, Clone, Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct ResourceOverride {
371 pub url: String,
372 #[serde(default, alias = "sha1", deserialize_with = "resource_path")]
376 pub file: Option<String>,
377 #[serde(rename = "ref", default)]
380 pub reference: Option<u64>,
381}
382
383#[derive(Debug, Clone, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct ScreencastFrameEvent {
387 pub page_id: String,
388 #[serde(alias = "sha1", deserialize_with = "required_resource_path")]
392 pub file: String,
393 pub width: u32,
394 pub height: u32,
395 pub timestamp: f64,
396}
397
398const RESOURCES_PREFIX: &str = "resources/";
402
403fn to_resource_path(mut raw: String) -> String {
406 if !raw.contains('/') {
407 raw.insert_str(0, RESOURCES_PREFIX);
408 }
409 raw
410}
411
412pub(crate) fn resource_path<'de, D>(
414 deserializer: D,
415) -> std::result::Result<Option<String>, D::Error>
416where
417 D: serde::Deserializer<'de>,
418{
419 Ok(Option::<String>::deserialize(deserializer)?.map(to_resource_path))
420}
421
422pub(crate) fn required_resource_path<'de, D>(
424 deserializer: D,
425) -> std::result::Result<String, D::Error>
426where
427 D: serde::Deserializer<'de>,
428{
429 Ok(to_resource_path(String::deserialize(deserializer)?))
430}
431
432#[derive(Debug, Clone, Deserialize)]
434#[serde(rename_all = "camelCase")]
435pub struct ActionError {
436 #[serde(default)]
437 pub name: String,
438 #[serde(default)]
439 pub message: String,
440}
441
442#[derive(Debug, Clone, Copy, Deserialize)]
445pub struct Point {
446 pub x: f64,
447 pub y: f64,
448}