1#![forbid(unsafe_code)]
4
5use js_sys::{
6 Array, Date, Error, Function, JsString, Map, Object, Promise, Reflect, RegExp, Set, Symbol,
7 WeakSet,
8};
9use std::{
10 collections::{BTreeMap, BTreeSet, HashSet},
11 fmt::{Debug, Display, Formatter, Result as FmtResult},
12 rc::Rc,
13};
14use wasm_bindgen::{JsCast, JsValue};
15use web_sys::{Document, Element, Window};
16
17pub trait Pretty {
18 fn pretty(&self) -> Prettified;
19}
20
21impl<T> Pretty for T
22where
23 T: AsRef<JsValue>,
24{
25 fn pretty(&self) -> Prettified {
26 Prettified {
27 value: self.as_ref().to_owned(),
28 seen: WeakSet::new(),
29 skip: Default::default(),
30 }
31 }
32}
33
34pub struct Prettified {
36 value: JsValue,
38 seen: WeakSet,
41 skip: Rc<HashSet<String>>,
43}
44
45impl Prettified {
46 pub fn skip_property(&mut self, name: &str) -> &mut Self {
49 let mut with_name = HashSet::to_owned(&self.skip);
50 with_name.insert(name.to_owned());
51 self.skip = Rc::new(with_name);
52 self
53 }
54
55 fn child(&self, v: &JsValue) -> Self {
56 Self { seen: self.seen.clone(), skip: self.skip.clone(), value: v.as_ref().clone() }
57 }
58
59 }
61
62impl Debug for Prettified {
63 fn fmt(&self, f: &mut Formatter) -> FmtResult {
64 let mut _reset = None;
67 if let Some(obj) = self.value.dyn_ref::<Object>() {
68 if self.seen.has(obj) {
69 return write!(f, "[Cycle]");
70 }
71
72 self.seen.add(obj);
73 _reset = Some(scopeguard::guard(obj.to_owned(), |obj| {
74 self.seen.delete(&obj);
75 }));
76 }
77
78 if self.value.is_null() {
79 write!(f, "null")
80 } else if self.value.is_undefined() {
81 write!(f, "undefined")
82 } else if self.value.dyn_ref::<Function>().is_some() {
83 JsFunction.fmt(f)
84 } else if self.value.dyn_ref::<Promise>().is_some() {
85 write!(f, "[Promise]")
86 } else if self.value.dyn_ref::<Document>().is_some() {
87 write!(f, "[Document]")
88 } else if self.value.dyn_ref::<Window>().is_some() {
89 write!(f, "[Window]")
90 } else if let Some(s) = self.value.dyn_ref::<JsString>() {
91 write!(f, "{:?}", s.as_string().unwrap())
92 } else if let Some(n) = self.value.as_f64() {
93 write!(f, "{}", n)
94 } else if let Some(b) = self.value.as_bool() {
95 write!(f, "{:?}", b)
96 } else if let Some(d) = self.value.dyn_ref::<Date>() {
97 write!(f, "{}", d.to_iso_string().as_string().unwrap())
98 } else if let Some(d) = self.value.dyn_ref::<Element>() {
99 let name = d.tag_name().to_ascii_lowercase();
100 let (mut class, mut id) = (d.class_name(), d.id());
101 if !class.is_empty() {
102 class.insert_str(0, " .");
103 }
104 if !id.is_empty() {
105 id.insert_str(0, " #");
106 }
107 write!(f, "<{}{}{}/>", name, id, class)
108 } else if let Some(e) = self.value.dyn_ref::<Error>() {
109 write!(f, "Error: {}", e.to_string().as_string().unwrap())
110 } else if let Some(r) = self.value.dyn_ref::<RegExp>() {
111 write!(f, "/{}/", r.to_string().as_string().unwrap())
112 } else if let Some(s) = self.value.dyn_ref::<Symbol>() {
113 write!(f, "{}", s.to_string().as_string().unwrap())
114 } else if let Some(a) = self.value.dyn_ref::<Array>() {
115 let mut f = f.debug_list();
116 for val in a.iter() {
117 f.entry(&self.child(&val));
118 }
119 f.finish()
120 } else if let Some(s) = self.value.dyn_ref::<Set>() {
121 let mut f = f.debug_set();
122 let entries = s.entries();
123 while let Ok(next) = entries.next() {
124 if next.done() {
125 break;
126 }
127 f.entry(&self.child(&next.value()));
128 }
129 f.finish()
130 } else if let Some(m) = self.value.dyn_ref::<Map>() {
131 let mut f = f.debug_map();
132 let keys = m.keys();
133 while let Ok(next) = keys.next() {
134 if next.done() {
135 break;
136 }
137 let key = next.value();
138 let value = m.get(&key);
139
140 f.entry(&self.child(&key), &self.child(&value));
141 }
142
143 f.finish()
144 } else if let Some(obj) = self.value.dyn_ref::<Object>() {
145 let mut proto = obj.clone();
146 let mut props_seen = HashSet::new();
147 let name = obj.constructor().name().as_string().unwrap();
148 let mut f = f.debug_struct(&name);
149
150 loop {
151 let mut functions = BTreeSet::new();
152 let mut props = BTreeMap::new();
153
154 for raw_key in Object::get_own_property_names(&proto).iter() {
155 let key = raw_key.as_string().expect("object keys are always strings");
156 if (key.starts_with("__") && key.ends_with("__"))
157 || props_seen.contains(&key)
158 || functions.contains(&key)
159 || self.skip.contains(&key)
160 {
161 continue;
162 }
163
164 if let Ok(value) = Reflect::get(&obj, &raw_key) {
165 props_seen.insert(key.clone());
166 if value.is_function() {
167 functions.insert(key);
168 } else {
169 props.insert(key, self.child(&value));
170 }
171 }
172 }
173
174 for (key, value) in props {
175 f.field(&key, &value);
176 }
177
178 for key in functions {
179 f.field(&key, &JsFunction);
180 }
181
182 proto = Object::get_prototype_of(proto.as_ref());
183 if proto.is_falsy() || proto.constructor().name().as_string().unwrap() == "Object" {
184 break;
186 }
187 }
188
189 f.finish()
190 } else {
191 write!(f, "unknown ({:?})", &self.value)
192 }
193 }
194}
195
196impl Display for Prettified {
197 fn fmt(&self, f: &mut Formatter) -> FmtResult {
198 write!(f, "{:#?}", self)
199 }
200}
201
202struct JsFunction;
203impl Debug for JsFunction {
204 fn fmt(&self, f: &mut Formatter) -> FmtResult {
205 write!(f, "[Function]")
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use futures::channel::oneshot::channel;
213 use wasm_bindgen::closure::Closure;
214 use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
215 use web_sys::{Event, EventTarget};
216
217 wasm_bindgen_test_configure!(run_in_browser);
218
219 #[wasm_bindgen_test]
220 fn cycle_is_broken() {
221 let with_cycles = js_sys::Function::new_no_args(
222 r#"
223 let root = { child: { nested: [] } };
224 root.child.nested.push(root);
225 return root;
226 "#,
227 )
228 .call0(&JsValue::null())
229 .unwrap();
230
231 assert_eq!(
232 with_cycles.pretty().to_string(),
233 r#"Object {
234 child: Object {
235 nested: [
236 [Cycle],
237 ],
238 },
239}"#
240 );
241 }
242
243 #[wasm_bindgen_test]
244 fn repeated_siblings_are_not_cycles() {
245 let with_siblings = js_sys::Function::new_no_args(
246 r#"
247 let root = { child: { nested: [] } };
248 let repeated_child = { foo: "bar" };
249 root.child.nested.push(repeated_child);
250 root.child.nested.push(repeated_child);
251 return root;
252 "#,
253 )
254 .call0(&JsValue::null())
255 .unwrap();
256
257 assert_eq!(
258 with_siblings.pretty().to_string(),
259 r#"Object {
260 child: Object {
261 nested: [
262 Object {
263 foo: "bar",
264 },
265 Object {
266 foo: "bar",
267 },
268 ],
269 },
270}"#
271 );
272 }
273
274 #[wasm_bindgen_test]
275 async fn live_keyboard_event() {
276 let window = web_sys::window().unwrap();
278 let document = window.document().unwrap();
279 let input = document.create_element("input").unwrap();
280 document.body().unwrap().append_child(input.as_ref()).unwrap();
282
283 let (send, recv) = channel();
285 let callback = Closure::once_into_js(move |ev: Event| {
286 send.send(ev).unwrap();
287 });
288 let target: &EventTarget = input.as_ref();
289 let event_type = "keydown";
290 target.add_event_listener_with_callback(event_type, callback.dyn_ref().unwrap()).unwrap();
291
292 let sent_event = web_sys::KeyboardEvent::new_with_keyboard_event_init_dict(
294 event_type,
295 web_sys::KeyboardEventInit::new()
296 .char_code(b'F' as u32)
297 .bubbles(true)
298 .cancelable(true)
299 .view(Some(&window)),
300 )
301 .unwrap();
302 let sent: &Event = sent_event.as_ref();
303 assert!(target.dispatch_event(sent).unwrap());
304
305 let received_event: Event = recv.await.unwrap();
307 assert_eq!(
309 received_event.pretty().skip_property("timeStamp").to_string(),
310 r#"KeyboardEvent {
311 isTrusted: false,
312 DOM_KEY_LOCATION_LEFT: 1,
313 DOM_KEY_LOCATION_NUMPAD: 3,
314 DOM_KEY_LOCATION_RIGHT: 2,
315 DOM_KEY_LOCATION_STANDARD: 0,
316 altKey: false,
317 charCode: 70,
318 code: "",
319 ctrlKey: false,
320 isComposing: false,
321 key: "",
322 keyCode: 0,
323 location: 0,
324 metaKey: false,
325 repeat: false,
326 shiftKey: false,
327 constructor: [Function],
328 getModifierState: [Function],
329 initKeyboardEvent: [Function],
330 detail: 0,
331 sourceCapabilities: null,
332 view: [Window],
333 which: 0,
334 initUIEvent: [Function],
335 AT_TARGET: 2,
336 BUBBLING_PHASE: 3,
337 CAPTURING_PHASE: 1,
338 NONE: 0,
339 bubbles: true,
340 cancelBubble: false,
341 cancelable: true,
342 composed: false,
343 currentTarget: null,
344 defaultPrevented: false,
345 eventPhase: 0,
346 path: [
347 <input/>,
348 <body/>,
349 <html/>,
350 [Document],
351 [Window],
352 ],
353 returnValue: true,
354 srcElement: <input/>,
355 target: <input/>,
356 type: "keydown",
357 composedPath: [Function],
358 initEvent: [Function],
359 preventDefault: [Function],
360 stopImmediatePropagation: [Function],
361 stopPropagation: [Function],
362}"#,
363 );
364 }
365}