viewpoint_core/page/console/
mod.rs

1//! Console message types and event handling.
2//!
3//! This module provides types for capturing JavaScript console output
4//! (console.log, console.error, etc.) from the page.
5
6// Allow dead code for console scaffolding (spec: console-events)
7
8use std::sync::Arc;
9
10use serde::{Deserialize, Serialize};
11use viewpoint_cdp::protocol::runtime::{ConsoleApiCalledEvent, ConsoleApiType, RemoteObject, StackTrace};
12use viewpoint_cdp::CdpConnection;
13
14/// Type of console message.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum ConsoleMessageType {
18    /// `console.log()`
19    Log,
20    /// `console.debug()`
21    Debug,
22    /// `console.info()`
23    Info,
24    /// `console.error()`
25    Error,
26    /// `console.warn()`
27    Warning,
28    /// `console.dir()`
29    Dir,
30    /// `console.dirxml()`
31    DirXml,
32    /// `console.table()`
33    Table,
34    /// `console.trace()`
35    Trace,
36    /// `console.clear()`
37    Clear,
38    /// `console.count()`
39    Count,
40    /// `console.assert()`
41    Assert,
42    /// `console.profile()`
43    Profile,
44    /// `console.profileEnd()`
45    ProfileEnd,
46    /// `console.group()` / `console.groupCollapsed()`
47    StartGroup,
48    /// `console.groupEnd()`
49    EndGroup,
50    /// `console.timeEnd()`
51    TimeEnd,
52}
53
54impl From<ConsoleApiType> for ConsoleMessageType {
55    fn from(api_type: ConsoleApiType) -> Self {
56        match api_type {
57            ConsoleApiType::Log => Self::Log,
58            ConsoleApiType::Debug => Self::Debug,
59            ConsoleApiType::Info => Self::Info,
60            ConsoleApiType::Error => Self::Error,
61            ConsoleApiType::Warning => Self::Warning,
62            ConsoleApiType::Dir => Self::Dir,
63            ConsoleApiType::Dirxml => Self::DirXml,
64            ConsoleApiType::Table => Self::Table,
65            ConsoleApiType::Trace => Self::Trace,
66            ConsoleApiType::Clear => Self::Clear,
67            ConsoleApiType::Count => Self::Count,
68            ConsoleApiType::Assert => Self::Assert,
69            ConsoleApiType::Profile => Self::Profile,
70            ConsoleApiType::ProfileEnd => Self::ProfileEnd,
71            ConsoleApiType::StartGroup | ConsoleApiType::StartGroupCollapsed => Self::StartGroup,
72            ConsoleApiType::EndGroup => Self::EndGroup,
73            ConsoleApiType::TimeEnd => Self::TimeEnd,
74        }
75    }
76}
77
78impl std::fmt::Display for ConsoleMessageType {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        let s = match self {
81            Self::Log => "log",
82            Self::Debug => "debug",
83            Self::Info => "info",
84            Self::Error => "error",
85            Self::Warning => "warning",
86            Self::Dir => "dir",
87            Self::DirXml => "dirxml",
88            Self::Table => "table",
89            Self::Trace => "trace",
90            Self::Clear => "clear",
91            Self::Count => "count",
92            Self::Assert => "assert",
93            Self::Profile => "profile",
94            Self::ProfileEnd => "profileEnd",
95            Self::StartGroup => "startGroup",
96            Self::EndGroup => "endGroup",
97            Self::TimeEnd => "timeEnd",
98        };
99        write!(f, "{s}")
100    }
101}
102
103/// Location information for a console message.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct ConsoleMessageLocation {
107    /// URL of the script that generated the message.
108    pub url: String,
109    /// Line number (0-based).
110    pub line_number: i32,
111    /// Column number (0-based).
112    pub column_number: i32,
113}
114
115/// A console message captured from the page.
116///
117/// Console messages are emitted when JavaScript code calls console methods
118/// like `console.log()`, `console.error()`, etc.
119///
120/// # Example
121///
122/// ```ignore
123/// page.on_console(|message| async move {
124///     println!("{}: {}", message.type_(), message.text());
125///     Ok(())
126/// }).await;
127/// ```
128#[derive(Debug, Clone)]
129pub struct ConsoleMessage {
130    /// Message type (log, error, warn, etc.).
131    message_type: ConsoleMessageType,
132    /// Message arguments as remote objects.
133    args: Vec<RemoteObject>,
134    /// Timestamp when the message was logged.
135    timestamp: f64,
136    /// Stack trace if available.
137    stack_trace: Option<StackTrace>,
138    /// Execution context ID.
139    execution_context_id: i64,
140    /// CDP connection for resolving arguments.
141    connection: Arc<CdpConnection>,
142    /// Session ID.
143    session_id: String,
144}
145
146impl ConsoleMessage {
147    /// Create a new console message from a CDP event.
148    pub(crate) fn from_event(
149        event: ConsoleApiCalledEvent,
150        connection: Arc<CdpConnection>,
151        session_id: String,
152    ) -> Self {
153        Self {
154            message_type: ConsoleMessageType::from(event.call_type),
155            args: event.args,
156            timestamp: event.timestamp,
157            stack_trace: event.stack_trace,
158            execution_context_id: event.execution_context_id,
159            connection,
160            session_id,
161        }
162    }
163
164    /// Get the message type.
165    ///
166    /// Returns the type of console call (log, error, warn, etc.).
167    pub fn type_(&self) -> ConsoleMessageType {
168        self.message_type
169    }
170
171    /// Get the formatted message text.
172    ///
173    /// This combines all arguments into a single string, similar to how
174    /// the browser console would display them.
175    pub fn text(&self) -> String {
176        self.args
177            .iter()
178            .map(|arg| {
179                if let Some(value) = &arg.value {
180                    format_value(value)
181                } else if let Some(description) = &arg.description {
182                    description.clone()
183                } else {
184                    arg.object_type.clone()
185                }
186            })
187            .collect::<Vec<_>>()
188            .join(" ")
189    }
190
191    /// Get the raw message arguments.
192    ///
193    /// These are the arguments passed to the console method as JS handles.
194    /// Use `text()` for a formatted string representation.
195    pub fn args(&self) -> Vec<JsArg> {
196        self.args
197            .iter()
198            .map(|arg| JsArg {
199                object_type: arg.object_type.clone(),
200                subtype: arg.subtype.clone(),
201                class_name: arg.class_name.clone(),
202                value: arg.value.clone(),
203                description: arg.description.clone(),
204                object_id: arg.object_id.clone(),
205            })
206            .collect()
207    }
208
209    /// Get the source location of the console call.
210    ///
211    /// Returns `None` if no stack trace is available.
212    pub fn location(&self) -> Option<ConsoleMessageLocation> {
213        self.stack_trace.as_ref().and_then(|st| {
214            st.call_frames.first().map(|frame| ConsoleMessageLocation {
215                url: frame.url.clone(),
216                line_number: frame.line_number,
217                column_number: frame.column_number,
218            })
219        })
220    }
221
222    /// Get the timestamp when the message was logged.
223    ///
224    /// Returns the timestamp in milliseconds since Unix epoch.
225    pub fn timestamp(&self) -> f64 {
226        self.timestamp
227    }
228}
229
230/// A JavaScript argument from a console message.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct JsArg {
234    /// Object type (object, function, string, number, etc.).
235    pub object_type: String,
236    /// Object subtype (array, null, regexp, etc.).
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub subtype: Option<String>,
239    /// Object class name.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub class_name: Option<String>,
242    /// Primitive value or JSON representation.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub value: Option<serde_json::Value>,
245    /// String representation of the object.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub description: Option<String>,
248    /// Object ID for retrieving properties.
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub object_id: Option<String>,
251}
252
253impl JsArg {
254    /// Get a JSON value representation.
255    pub fn json_value(&self) -> Option<&serde_json::Value> {
256        self.value.as_ref()
257    }
258}
259
260/// Format a JSON value as a string for console output.
261fn format_value(value: &serde_json::Value) -> String {
262    match value {
263        serde_json::Value::Null => "null".to_string(),
264        serde_json::Value::Bool(b) => b.to_string(),
265        serde_json::Value::Number(n) => n.to_string(),
266        serde_json::Value::String(s) => s.clone(),
267        serde_json::Value::Array(arr) => {
268            let items: Vec<String> = arr.iter().map(format_value).collect();
269            format!("[{}]", items.join(", "))
270        }
271        serde_json::Value::Object(obj) => {
272            if obj.is_empty() {
273                "{}".to_string()
274            } else {
275                let pairs: Vec<String> = obj
276                    .iter()
277                    .map(|(k, v)| format!("{k}: {}", format_value(v)))
278                    .collect();
279                format!("{{{}}}", pairs.join(", "))
280            }
281        }
282    }
283}