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/// ```
123/// # #[cfg(feature = "integration")]
124/// # tokio_test::block_on(async {
125/// # use viewpoint_core::Browser;
126/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
127/// # let context = browser.new_context().await.unwrap();
128/// # let page = context.new_page().await.unwrap();
129///
130/// page.on_console(|message| async move {
131///     println!("{}: {}", message.type_(), message.text());
132/// }).await;
133/// # });
134/// ```
135#[derive(Debug, Clone)]
136pub struct ConsoleMessage {
137    /// Message type (log, error, warn, etc.).
138    message_type: ConsoleMessageType,
139    /// Message arguments as remote objects.
140    args: Vec<RemoteObject>,
141    /// Timestamp when the message was logged.
142    timestamp: f64,
143    /// Stack trace if available.
144    stack_trace: Option<StackTrace>,
145    /// Execution context ID.
146    execution_context_id: i64,
147    /// CDP connection for resolving arguments.
148    connection: Arc<CdpConnection>,
149    /// Session ID.
150    session_id: String,
151}
152
153impl ConsoleMessage {
154    /// Create a new console message from a CDP event.
155    pub(crate) fn from_event(
156        event: ConsoleApiCalledEvent,
157        connection: Arc<CdpConnection>,
158        session_id: String,
159    ) -> Self {
160        Self {
161            message_type: ConsoleMessageType::from(event.call_type),
162            args: event.args,
163            timestamp: event.timestamp,
164            stack_trace: event.stack_trace,
165            execution_context_id: event.execution_context_id,
166            connection,
167            session_id,
168        }
169    }
170
171    /// Get the message type.
172    ///
173    /// Returns the type of console call (log, error, warn, etc.).
174    pub fn type_(&self) -> ConsoleMessageType {
175        self.message_type
176    }
177
178    /// Get the formatted message text.
179    ///
180    /// This combines all arguments into a single string, similar to how
181    /// the browser console would display them.
182    pub fn text(&self) -> String {
183        self.args
184            .iter()
185            .map(|arg| {
186                if let Some(value) = &arg.value {
187                    format_value(value)
188                } else if let Some(description) = &arg.description {
189                    description.clone()
190                } else {
191                    arg.object_type.clone()
192                }
193            })
194            .collect::<Vec<_>>()
195            .join(" ")
196    }
197
198    /// Get the raw message arguments.
199    ///
200    /// These are the arguments passed to the console method as JS handles.
201    /// Use `text()` for a formatted string representation.
202    pub fn args(&self) -> Vec<JsArg> {
203        self.args
204            .iter()
205            .map(|arg| JsArg {
206                object_type: arg.object_type.clone(),
207                subtype: arg.subtype.clone(),
208                class_name: arg.class_name.clone(),
209                value: arg.value.clone(),
210                description: arg.description.clone(),
211                object_id: arg.object_id.clone(),
212            })
213            .collect()
214    }
215
216    /// Get the source location of the console call.
217    ///
218    /// Returns `None` if no stack trace is available.
219    pub fn location(&self) -> Option<ConsoleMessageLocation> {
220        self.stack_trace.as_ref().and_then(|st| {
221            st.call_frames.first().map(|frame| ConsoleMessageLocation {
222                url: frame.url.clone(),
223                line_number: frame.line_number,
224                column_number: frame.column_number,
225            })
226        })
227    }
228
229    /// Get the timestamp when the message was logged.
230    ///
231    /// Returns the timestamp in milliseconds since Unix epoch.
232    pub fn timestamp(&self) -> f64 {
233        self.timestamp
234    }
235}
236
237/// A JavaScript argument from a console message.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct JsArg {
241    /// Object type (object, function, string, number, etc.).
242    pub object_type: String,
243    /// Object subtype (array, null, regexp, etc.).
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub subtype: Option<String>,
246    /// Object class name.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub class_name: Option<String>,
249    /// Primitive value or JSON representation.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub value: Option<serde_json::Value>,
252    /// String representation of the object.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub description: Option<String>,
255    /// Object ID for retrieving properties.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub object_id: Option<String>,
258}
259
260impl JsArg {
261    /// Get a JSON value representation.
262    pub fn json_value(&self) -> Option<&serde_json::Value> {
263        self.value.as_ref()
264    }
265}
266
267/// Format a JSON value as a string for console output.
268fn format_value(value: &serde_json::Value) -> String {
269    match value {
270        serde_json::Value::Null => "null".to_string(),
271        serde_json::Value::Bool(b) => b.to_string(),
272        serde_json::Value::Number(n) => n.to_string(),
273        serde_json::Value::String(s) => s.clone(),
274        serde_json::Value::Array(arr) => {
275            let items: Vec<String> = arr.iter().map(format_value).collect();
276            format!("[{}]", items.join(", "))
277        }
278        serde_json::Value::Object(obj) => {
279            if obj.is_empty() {
280                "{}".to_string()
281            } else {
282                let pairs: Vec<String> = obj
283                    .iter()
284                    .map(|(k, v)| format!("{k}: {}", format_value(v)))
285                    .collect();
286                format!("{{{}}}", pairs.join(", "))
287            }
288        }
289    }
290}