Skip to main content

playwright_rs_trace/
action.rs

1//! Action — reassembled from `before` + optional `input` + zero-or-more
2//! `log` + `after` events sharing a `call_id`.
3//!
4//! [`ActionStream`] consumes a stream of [`TraceEvent`]s and yields
5//! [`Action`]s in `after`-arrival order. Truncated actions (no matching
6//! `after` event) are emitted at end-of-stream rather than discarded —
7//! useful when diagnosing crashed-mid-action traces.
8
9use crate::error::Result;
10use crate::event::{ActionError, AfterEvent, BeforeEvent, InputEvent, LogEvent, Point, TraceEvent};
11use serde_json::Value;
12use std::collections::HashMap;
13
14/// A logical action — `class.method` call as recorded in the trace.
15#[derive(Debug, Clone)]
16pub struct Action {
17    pub call_id: String,
18    pub parent_id: Option<String>,
19    pub class: String,
20    pub method: String,
21    pub title: Option<String>,
22    pub page_id: Option<String>,
23    pub start_time: f64,
24    /// `None` for actions whose matching `after` event never arrived
25    /// (truncated trace).
26    pub end_time: Option<f64>,
27    pub params: Value,
28    pub result: Option<Value>,
29    pub error: Option<ActionError>,
30    pub logs: Vec<LogLine>,
31    pub input: Option<InputEvent>,
32    pub before_snapshot: Option<String>,
33    pub after_snapshot: Option<String>,
34    pub point: Option<Point>,
35}
36
37/// One log line attached to an action via the `log` event.
38#[derive(Debug, Clone)]
39pub struct LogLine {
40    pub time: f64,
41    pub message: String,
42}
43
44impl From<LogEvent> for LogLine {
45    fn from(value: LogEvent) -> Self {
46        Self {
47            time: value.time,
48            message: value.message,
49        }
50    }
51}
52
53/// Streaming reassembly of [`Action`]s from a [`TraceEvent`] iterator.
54/// Use [`crate::TraceReader::actions`] to construct the typical case;
55/// public here so callers can wrap their own custom event source.
56pub struct ActionStream<I> {
57    events: I,
58    pending: HashMap<String, ActionBuilder>,
59    /// Order of `call_id` insertion, used to drain truncated actions
60    /// in a deterministic order at end-of-stream.
61    pending_order: Vec<String>,
62    upstream_done: bool,
63}
64
65impl<I> ActionStream<I>
66where
67    I: Iterator<Item = Result<TraceEvent>>,
68{
69    pub fn new(events: I) -> Self {
70        Self {
71            events,
72            pending: HashMap::new(),
73            pending_order: Vec::new(),
74            upstream_done: false,
75        }
76    }
77}
78
79impl<I> Iterator for ActionStream<I>
80where
81    I: Iterator<Item = Result<TraceEvent>>,
82{
83    type Item = Result<Action>;
84
85    fn next(&mut self) -> Option<Self::Item> {
86        loop {
87            if self.upstream_done {
88                // Drain truncated actions one at a time.
89                while let Some(call_id) = self.pending_order.pop() {
90                    if let Some(builder) = self.pending.remove(&call_id) {
91                        return Some(Ok(builder.finalize_truncated()));
92                    }
93                }
94                return None;
95            }
96
97            let event = match self.events.next() {
98                Some(Ok(e)) => e,
99                Some(Err(e)) => return Some(Err(e)),
100                None => {
101                    self.upstream_done = true;
102                    continue;
103                }
104            };
105
106            match event {
107                TraceEvent::Before(b) => {
108                    let call_id = b.call_id.clone();
109                    if !self.pending.contains_key(&call_id) {
110                        self.pending_order.push(call_id.clone());
111                    }
112                    self.pending.insert(call_id, ActionBuilder::from_before(b));
113                }
114                TraceEvent::Input(i) => {
115                    if let Some(builder) = self.pending.get_mut(&i.call_id) {
116                        builder.input = Some(i);
117                    }
118                    // Orphan input (no matching `before`) is dropped
119                    // silently — typical for traces truncated at the
120                    // head.
121                }
122                TraceEvent::Log(l) => {
123                    if let Some(builder) = self.pending.get_mut(&l.call_id) {
124                        builder.logs.push(l.into());
125                    }
126                }
127                TraceEvent::After(a) => {
128                    if let Some(builder) = self.pending.remove(&a.call_id) {
129                        // Maintain pending_order: lazy removal at drain
130                        // time. The vector may carry stale entries for
131                        // already-finalised actions; the drain loop
132                        // skips them via the `pending.remove` check.
133                        return Some(Ok(builder.finalize(a)));
134                    }
135                    // Orphan after — ignore.
136                }
137                _ => {}
138            }
139        }
140    }
141}
142
143struct ActionBuilder {
144    call_id: String,
145    parent_id: Option<String>,
146    class: String,
147    method: String,
148    title: Option<String>,
149    page_id: Option<String>,
150    start_time: f64,
151    params: Value,
152    before_snapshot: Option<String>,
153    logs: Vec<LogLine>,
154    input: Option<InputEvent>,
155}
156
157impl ActionBuilder {
158    fn from_before(b: BeforeEvent) -> Self {
159        Self {
160            call_id: b.call_id,
161            parent_id: b.parent_id,
162            class: b.class,
163            method: b.method,
164            title: b.title,
165            page_id: b.page_id,
166            start_time: b.start_time,
167            params: b.params,
168            before_snapshot: b.before_snapshot,
169            logs: Vec::new(),
170            input: None,
171        }
172    }
173
174    fn finalize(self, a: AfterEvent) -> Action {
175        Action {
176            call_id: self.call_id,
177            parent_id: self.parent_id,
178            class: self.class,
179            method: self.method,
180            title: self.title,
181            page_id: self.page_id,
182            start_time: self.start_time,
183            end_time: Some(a.end_time),
184            params: self.params,
185            result: a.result,
186            error: a.error,
187            logs: self.logs,
188            input: self.input,
189            before_snapshot: self.before_snapshot,
190            after_snapshot: a.after_snapshot,
191            point: a.point,
192        }
193    }
194
195    fn finalize_truncated(self) -> Action {
196        Action {
197            call_id: self.call_id,
198            parent_id: self.parent_id,
199            class: self.class,
200            method: self.method,
201            title: self.title,
202            page_id: self.page_id,
203            start_time: self.start_time,
204            end_time: None,
205            params: self.params,
206            result: None,
207            error: None,
208            logs: self.logs,
209            input: self.input,
210            before_snapshot: self.before_snapshot,
211            after_snapshot: None,
212            point: None,
213        }
214    }
215}