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