orinium_browser/engine/js/
devtools.rs1use pixi_byte::vm::VM;
8use pixi_byte::{JSError, JSResult, JSValue};
9
10use super::{JsRuntime, with_host_mut};
11
12#[derive(Debug)]
15pub struct JsDevToolsRequest {
16 pub(crate) id: u64,
17 pub(crate) method: String,
18 pub(crate) params: String,
20}
21
22pub(crate) struct JsDevToolsCapability {
24 resolve: JSValue,
25}
26
27const DEVTOOLS_GLOBAL: &str = "__orinium_devtools";
28
29pub(super) fn install(engine: &mut pixi_byte::JSEngine) {
31 engine.global_mut().borrow_mut().set(
32 DEVTOOLS_GLOBAL.to_string(),
33 JSValue::from_native_function(inspect),
34 );
35}
36
37fn inspect(vm: &mut VM, args: Vec<JSValue>) -> JSResult<JSValue> {
38 let Some(method) = args.get(1).and_then(JSValue::as_string_owned) else {
39 return Err(JSError::TypeError(
40 "__orinium_devtools requires a method string".to_string(),
41 ));
42 };
43 let params = match args.get(2) {
44 Some(value) => {
45 if let Some(params) = value.as_string_owned() {
46 params
47 } else if value.is_undefined() || value.is_null() {
48 "{}".to_string()
49 } else {
50 return Err(JSError::TypeError(
51 "__orinium_devtools params must be a JSON string".to_string(),
52 ));
53 }
54 }
55 None => "{}".to_string(),
56 };
57
58 let promise_constructor = vm.global_object.borrow().get("Promise");
59 let Some(constructor) = promise_constructor.as_object() else {
60 return Err(JSError::InternalError(
61 "Promise constructor is unavailable".to_string(),
62 ));
63 };
64 let construct = constructor.borrow().get("__construct__");
65 let _ = with_host_mut(vm, |host| host.constructing_devtools_capability = None);
66 let promise = vm.call(
67 construct,
68 promise_constructor,
69 vec![JSValue::from_native_function(capture_capability)],
70 )?;
71 let capability = with_host_mut(vm, |host| host.constructing_devtools_capability.take())
72 .flatten()
73 .ok_or_else(|| JSError::InternalError("Failed to create DevTools Promise".to_string()))?;
74
75 let _ = with_host_mut(vm, |host| {
76 host.next_devtools_id = host.next_devtools_id.wrapping_add(1);
77 let id = host.next_devtools_id;
78 host.devtools_capabilities.insert(id, capability);
79 host.devtools_requests
80 .push(JsDevToolsRequest { id, method, params });
81 });
82 Ok(promise)
83}
84
85fn capture_capability(vm: &mut VM, args: Vec<JSValue>) -> JSResult<JSValue> {
86 let resolve = args.get(1).cloned().unwrap_or(JSValue::undefined());
87 let Some(()) = with_host_mut(vm, |host| {
88 host.constructing_devtools_capability = Some(JsDevToolsCapability { resolve });
89 }) else {
90 return Err(JSError::InternalError(
91 "DevTools host state is unavailable".to_string(),
92 ));
93 };
94 Ok(JSValue::undefined())
95}
96
97impl JsRuntime {
98 pub(crate) fn take_devtools_requests(&mut self) -> Vec<JsDevToolsRequest> {
101 with_host_mut(self.engine.vm(), |host| {
102 std::mem::take(&mut host.devtools_requests)
103 })
104 .unwrap_or_default()
105 }
106
107 pub(crate) fn resolve_devtools(&mut self, id: u64, result: String) {
110 let Some(capability) = with_host_mut(self.engine.vm(), |host| {
111 host.devtools_capabilities.remove(&id)
112 })
113 .flatten() else {
114 return;
115 };
116 if let Err(err) = self.engine.call(
117 capability.resolve,
118 JSValue::undefined(),
119 vec![JSValue::from_string(result)],
120 ) {
121 log::info!("JS error while resolving devtools request: {}", err);
122 }
123 self.perform_microtask_checkpoint();
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use std::time::{Duration, Instant};
130
131 use super::super::{JsProcessor, JsTask};
132 use crate::engine::html::parser::Parser as HtmlParser;
133 use crate::engine::layouter::dom_snapshot::DomSnapshot;
134
135 fn wait_for_result(processor: &JsProcessor) -> crate::engine::js::JsTaskResult {
136 let deadline = Instant::now() + Duration::from_secs(5);
137 loop {
138 if let Some(result) = processor.try_receive() {
139 return result;
140 }
141 assert!(
142 Instant::now() < deadline,
143 "JS result did not arrive before the timeout"
144 );
145 std::thread::sleep(Duration::from_millis(1));
146 }
147 }
148
149 #[test]
150 fn devtools_request_round_trip_resolves_the_promise() {
151 let dom = HtmlParser::new("<html><body></body></html>").parse();
152 let (snapshot, _) = DomSnapshot::from_tree(&dom.root);
153 let processor = JsProcessor::new(snapshot);
154
155 processor.send(JsTask::RunScript {
156 source: r#"
157 globalThis.__result = null;
158 __orinium_devtools("getVersion").then(function (json) {
159 globalThis.__result = json;
160 });
161 "#
162 .to_string(),
163 });
164
165 let result = wait_for_result(&processor);
166 assert_eq!(result.devtools_requests.len(), 1);
167 let request = &result.devtools_requests[0];
168 assert_eq!(request.method, "getVersion");
169 assert_eq!(request.params, "{}");
170
171 processor.send(JsTask::ResolveDevTools {
172 id: request.id,
173 result: r#"{"ok":true,"data":{"version":7}}"#.to_string(),
174 });
175 let _ = wait_for_result(&processor);
176
177 processor.send(JsTask::RunScript {
178 source: r#"
179 if (globalThis.__result !== '{"ok":true,"data":{"version":7}}') {
180 throw new Error("unexpected result: " + globalThis.__result);
181 }
182 document.body.setAttribute("data-ok", "yes");
183 "#
184 .to_string(),
185 });
186 let final_result = wait_for_result(&processor);
187 assert!(
188 final_result.needs_redraw,
189 "the verification script must run without throwing"
190 );
191 }
192
193 #[test]
194 fn devtools_rejects_non_string_method() {
195 let dom = HtmlParser::new("<html><body></body></html>").parse();
196 let (snapshot, _) = DomSnapshot::from_tree(&dom.root);
197 let processor = JsProcessor::new(snapshot);
198
199 processor.send(JsTask::RunScript {
200 source: r#"
201 try {
202 __orinium_devtools(42);
203 throw new Error("expected __orinium_devtools to reject numbers");
204 } catch (error) {
205 document.body.setAttribute("data-rejected", "yes");
206 }
207 "#
208 .to_string(),
209 });
210
211 let result = wait_for_result(&processor);
212 assert!(result.devtools_requests.is_empty());
213 assert!(
214 result.needs_redraw,
215 "the catch block must have run without throwing"
216 );
217 }
218}