1use alloc::boxed::Box;
8use alloc::rc::Rc;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use base64::Engine;
12use core::cell::RefCell;
13use core::future::poll_fn;
14use core::pin::Pin;
15use core::task::Poll;
16
17use http::Response;
18
19use crate::batch::{Runtime, in_runtime};
20use crate::function_registry::FUNCTION_REGISTRY;
21use crate::ipc::{IPCMessage, decode_data};
22use crate::runtime::{
23 DriverCommand, DriverCommandReceiver, DriverCommandSender, DriverCommandWeakSender, IPCSenders,
24 Inbound, InboundSendError, WryIPC, dispatch_inbound_message,
25};
26
27struct WryBindgenResponder {
28 respond: Box<dyn FnOnce(Response<Vec<u8>>)>,
29}
30
31impl<F> From<F> for WryBindgenResponder
32where
33 F: FnOnce(Response<Vec<u8>>) + 'static,
34{
35 fn from(respond: F) -> Self {
36 Self {
37 respond: Box::new(respond),
38 }
39 }
40}
41
42impl WryBindgenResponder {
43 fn respond(self, response: Response<Vec<u8>>) {
44 (self.respond)(response);
45 }
46
47 fn respond_ipc(self, response: IPCMessage) {
48 let body = response.data();
49 let engine = base64::engine::general_purpose::STANDARD;
51 let body_base64 = engine.encode(body);
52 self.respond(
53 http::Response::builder()
54 .status(200)
55 .header("Content-Type", "text/plain")
56 .body(body_base64.into_bytes())
57 .expect("Failed to build response"),
58 );
59 }
60}
61
62fn decode_request_data(request: &http::Request<Vec<u8>>) -> Option<IPCMessage> {
64 if let Some(header_value) = request.headers().get("dioxus-data") {
65 return decode_data(header_value.as_bytes());
66 }
67 None
68}
69
70enum WebviewLoadingState {
72 Pending {
76 pending_ipc: Option<IPCMessage>,
77 acquire_lock: bool,
78 },
79 Loaded,
81}
82
83impl Default for WebviewLoadingState {
84 fn default() -> Self {
85 WebviewLoadingState::Pending {
86 pending_ipc: None,
87 acquire_lock: false,
88 }
89 }
90}
91
92struct WebviewState {
94 messages: WebviewMessageLayer,
96 loading_state: WebviewLoadingState,
98}
99
100struct WebviewMessageLayer {
110 current_xhr: Option<WryBindgenResponder>,
111 sender: IPCSenders,
113}
114
115impl WebviewState {
116 fn new(sender: IPCSenders) -> Self {
118 Self {
119 messages: WebviewMessageLayer::new(sender),
120 loading_state: WebviewLoadingState::default(),
121 }
122 }
123
124 fn handle_driver_command(&mut self, command: DriverCommand) -> DriverAction {
125 match command {
126 DriverCommand::AcquireLock => self.handle_acquire_lock(),
127 DriverCommand::SendIpc(ipc_msg) => {
128 self.handle_ipc_message(ipc_msg);
129 DriverAction::None
130 }
131 DriverCommand::ReleaseLock => {
132 self.messages.release_lock();
133 DriverAction::None
134 }
135 }
136 }
137
138 fn handle_ipc_message(&mut self, ipc_msg: IPCMessage) {
139 if let WebviewLoadingState::Pending { pending_ipc, .. } = &mut self.loading_state {
140 assert!(
141 pending_ipc.replace(ipc_msg).is_none(),
142 "multiple Rust IPC messages queued before webview load"
143 );
144 return;
145 }
146
147 self.messages.receive_rust_message(ipc_msg);
148 }
149
150 fn handle_acquire_lock(&mut self) -> DriverAction {
151 if let WebviewLoadingState::Pending { acquire_lock, .. } = &mut self.loading_state {
152 *acquire_lock = true;
153 return DriverAction::None;
154 }
155
156 DriverAction::RequestJsLock
157 }
158
159 fn mark_loaded(&mut self) -> bool {
160 if let WebviewLoadingState::Pending {
161 pending_ipc,
162 acquire_lock,
163 } = std::mem::replace(&mut self.loading_state, WebviewLoadingState::Loaded)
164 {
165 if let Some(msg) = pending_ipc {
166 self.messages.receive_rust_message(msg);
167 }
168 return acquire_lock;
169 }
170
171 false
172 }
173}
174
175enum DriverAction {
176 None,
177 RequestJsLock,
178}
179
180impl DriverAction {
181 fn run(self, evaluate_script: &mut impl FnMut(&str)) {
182 match self {
183 DriverAction::None => {}
184 DriverAction::RequestJsLock => {
185 evaluate_script("window.__wry_acquire_handler_lock()");
186 }
187 }
188 }
189}
190
191impl WebviewMessageLayer {
192 fn new(sender: IPCSenders) -> Self {
193 Self {
194 current_xhr: None,
195 sender,
196 }
197 }
198
199 fn receive_js_message(&mut self, msg: IPCMessage, responder: WryBindgenResponder) {
200 self.park_and_forward(responder, Inbound::Message(msg));
201 }
202
203 fn receive_lock_request(&mut self, responder: WryBindgenResponder) {
204 self.park_and_forward(responder, Inbound::LockReady);
205 }
206
207 fn park_and_forward(&mut self, responder: WryBindgenResponder, inbound: Inbound) {
208 assert!(
209 self.current_xhr.is_none(),
210 "JS parked a new XHR while another JS XHR is waiting for Rust"
211 );
212 self.current_xhr = Some(responder);
213 match self.sender.send(inbound) {
214 Ok(()) => {}
215 Err(InboundSendError::Closed) => {
216 let responder = self.take_parked_xhr();
217 responder.respond(error_response());
218 }
219 Err(InboundSendError::Occupied) => {
220 panic!("inbound IPC slot occupied while parking a JS XHR")
221 }
222 }
223 }
224
225 fn receive_rust_message(&mut self, ipc_msg: IPCMessage) {
226 let responder = self.take_parked_xhr();
230 responder.respond_ipc(ipc_msg);
231 }
232
233 fn release_lock(&mut self) {
234 let responder = self.take_parked_xhr();
235 responder.respond(blank_response());
236 }
237
238 fn take_parked_xhr(&mut self) -> WryBindgenResponder {
241 self.current_xhr.take().unwrap()
242 }
243}
244
245pub struct ProtocolHandler {
249 webview: Rc<RefCell<WebviewState>>,
250 driver_commands: DriverCommandWeakSender,
251}
252
253impl ProtocolHandler {
254 pub fn handle_request<R>(
265 &self,
266 protocol: &str,
267 request: &http::Request<Vec<u8>>,
268 responder: R,
269 ) -> Option<R>
270 where
271 R: FnOnce(Response<Vec<u8>>) + 'static,
272 {
273 let webviews = &self.webview;
274
275 let protocol_prefix = format!("{protocol}://index.html");
276 let android_prefix = format!("https://{protocol}.index.html");
277 let windows_prefix = format!("http://{protocol}.index.html");
278
279 let uri = request.uri().to_string();
280 let real_path = uri
281 .strip_prefix(&protocol_prefix)
282 .or_else(|| uri.strip_prefix(&windows_prefix))
283 .or_else(|| uri.strip_prefix(&android_prefix))
284 .unwrap_or(&uri);
285 let real_path = real_path.trim_matches('/');
286
287 let Some(path_without_wbg) = real_path.strip_prefix("__wbg__/") else {
288 return Some(responder);
290 };
291
292 if let Some(path_without_snippets) = path_without_wbg.strip_prefix("snippets/") {
294 let responder = WryBindgenResponder::from(responder);
295 if let Some(content) = FUNCTION_REGISTRY.get_module(path_without_snippets) {
296 responder.respond(module_response(content));
297 return None;
298 }
299 responder.respond(not_found_response());
300 return None;
301 }
302
303 if path_without_wbg == "init.js" {
304 let responder = WryBindgenResponder::from(responder);
305 responder.respond(module_response(&init_script()));
306 return None;
307 }
308
309 if path_without_wbg == "initialized" {
310 let acquire_lock = webviews.borrow_mut().mark_loaded();
311 if acquire_lock {
312 self.driver_commands.send(DriverCommand::AcquireLock);
313 }
314 let responder = WryBindgenResponder::from(responder);
315 responder.respond(blank_response());
316 return None;
317 }
318
319 if path_without_wbg == "handler" {
321 let responder = WryBindgenResponder::from(responder);
322 let mut webview_state = webviews.borrow_mut();
323 if request.headers().get("wry-bindgen-lock").is_some() {
324 webview_state.messages.receive_lock_request(responder);
325 return None;
326 }
327 let Some(msg) = decode_request_data(request) else {
328 responder.respond(error_response());
329 return None;
330 };
331 webview_state.messages.receive_js_message(msg, responder);
332 return None;
333 }
334
335 Some(responder)
336 }
337}
338
339fn init_script() -> String {
343 const INITIALIZATION_SCRIPT: &str = include_str!("./js/main.js");
345 let collect_functions = FUNCTION_REGISTRY.script();
346 format!("{INITIALIZATION_SCRIPT}\n{collect_functions}")
347}
348
349pub struct WryBindgen {
354 webview: Rc<RefCell<WebviewState>>,
355 ipc: WryIPC,
356 driver_commands: DriverCommandReceiver,
357 weak_driver_commands: DriverCommandWeakSender,
358}
359
360impl WryBindgen {
361 pub fn new() -> Self {
363 let (ipc, senders, driver_commands) = WryIPC::new();
364 let weak_driver_commands = ipc.command_sender().downgrade();
365 Self {
366 webview: Rc::new(RefCell::new(WebviewState::new(senders))),
367 ipc,
368 driver_commands,
369 weak_driver_commands,
370 }
371 }
372
373 pub fn protocol_handler(&self) -> ProtocolHandler {
375 ProtocolHandler {
376 webview: self.webview.clone(),
377 driver_commands: self.weak_driver_commands.clone(),
378 }
379 }
380
381 pub fn split(self) -> (WryBindgenRuntime, WryBindgenDriver) {
383 (
384 WryBindgenRuntime { ipc: self.ipc },
385 WryBindgenDriver {
386 webview: self.webview,
387 commands: self.driver_commands,
388 },
389 )
390 }
391}
392
393impl Default for WryBindgen {
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399struct JsLockGuard {
406 commands: DriverCommandSender,
407}
408
409impl JsLockGuard {
410 fn acquire(ipc: &WryIPC) -> Self {
411 Self {
412 commands: ipc.command_sender(),
413 }
414 }
415}
416
417impl Drop for JsLockGuard {
418 fn drop(&mut self) {
419 self.commands.send(DriverCommand::ReleaseLock);
420 }
421}
422
423pub struct WryBindgenRuntime {
425 ipc: WryIPC,
426}
427
428impl WryBindgenRuntime {
429 pub fn run<F, Fut>(
432 self,
433 app: F,
434 ) -> impl IntoFuture<Output = (), IntoFuture: 'static> + Send + 'static
435 where
436 F: FnOnce() -> Fut + Send + 'static,
437 Fut: core::future::Future<Output = ()> + 'static,
438 {
439 struct RuntimeFuture<F, Fut> {
440 app: F,
441 ipc: WryIPC,
442 phantom: core::marker::PhantomData<fn(Fut)>,
443 }
444
445 impl<F, Fut> RuntimeFuture<F, Fut> {
446 fn new(app: F, ipc: WryIPC) -> Self {
447 Self {
448 app,
449 ipc,
450 phantom: core::marker::PhantomData,
451 }
452 }
453 }
454
455 impl<F, Fut> IntoFuture for RuntimeFuture<F, Fut>
456 where
457 F: FnOnce() -> Fut + Send + 'static,
458 Fut: core::future::Future<Output = ()> + 'static,
459 {
460 type IntoFuture = Pin<Box<dyn core::future::Future<Output = ()>>>;
461 type Output = ();
462
463 fn into_future(self) -> Self::IntoFuture {
464 let Self { app, ipc, .. } = self;
465 let mut runtime = Some(Runtime::new(ipc));
466 let mut app = Some(app);
467 let mut run_app = None::<Pin<Box<Fut>>>;
468
469 let poll_driver = poll_fn(move |ctx| {
475 let mut just_polled_app = false;
476 loop {
477 let Some(rt) = runtime.as_ref() else {
478 return Poll::Ready(());
479 };
480 match rt.ipc().poll_recv(ctx) {
481 Poll::Ready(Some(Inbound::Message(msg))) => {
485 let owned = runtime.take().expect("runtime available");
486 let (owned, _) =
487 in_runtime(owned, || dispatch_inbound_message(&msg));
488 runtime = Some(owned);
489 just_polled_app = false;
490 }
491 Poll::Ready(Some(Inbound::LockReady)) => {
494 let _guard = JsLockGuard::acquire(rt.ipc());
495 if run_app.is_none() {
496 run_app = Some(Box::pin(app
497 .take()
498 .expect("app constructor called once")(
499 )));
500 }
501 let owned = runtime.take().expect("runtime available");
502 let (owned, poll_result) = in_runtime(owned, || {
503 run_app
504 .as_mut()
505 .expect("app future must exist")
506 .as_mut()
507 .poll(ctx)
508 });
509 runtime = Some(owned);
510 if poll_result.is_ready() {
511 return Poll::Ready(());
512 }
513 just_polled_app = true;
514 }
515 Poll::Ready(None) => return Poll::Ready(()),
516 Poll::Pending => {
517 if !just_polled_app {
521 rt.ipc().send_acquire_lock();
522 }
523 return Poll::Pending;
524 }
525 }
526 }
527 });
528
529 Box::pin(poll_driver)
530 }
531 }
532
533 RuntimeFuture::new(app, self.ipc)
534 }
535}
536
537pub struct WryBindgenDriver {
539 webview: Rc<RefCell<WebviewState>>,
540 commands: DriverCommandReceiver,
541}
542
543impl WryBindgenDriver {
544 pub fn with_evaluate_script(
549 self,
550 evaluate_script: impl FnMut(&str) + 'static,
551 ) -> WryBindgenWebviewDriver {
552 WryBindgenWebviewDriver {
553 driver: self,
554 evaluate_script: Box::new(evaluate_script),
555 }
556 }
557}
558
559pub struct WryBindgenWebviewDriver {
561 driver: WryBindgenDriver,
562 evaluate_script: Box<dyn FnMut(&str)>,
563}
564
565impl WryBindgenWebviewDriver {
566 pub fn poll(&mut self, cx: &mut core::task::Context<'_>) -> Poll<()> {
569 loop {
570 match self.driver.commands.poll_recv(cx) {
571 Poll::Ready(Some(command)) => {
572 let action = self
573 .driver
574 .webview
575 .borrow_mut()
576 .handle_driver_command(command);
577 action.run(&mut self.evaluate_script);
578 }
579 Poll::Ready(None) => return Poll::Ready(()),
580 Poll::Pending => return Poll::Pending,
581 }
582 }
583 }
584}
585
586fn blank_response() -> http::Response<Vec<u8>> {
588 http::Response::builder()
589 .status(200)
590 .body(vec![])
591 .expect("Failed to build blank response")
592}
593
594fn error_response() -> http::Response<Vec<u8>> {
596 http::Response::builder()
597 .status(400)
598 .body(vec![])
599 .expect("Failed to build error response")
600}
601
602fn module_response(content: &str) -> http::Response<Vec<u8>> {
604 http::Response::builder()
605 .status(200)
606 .header("Content-Type", "application/javascript")
607 .header("access-control-allow-origin", "*")
608 .body(content.as_bytes().to_vec())
609 .expect("Failed to build module response")
610}
611
612fn not_found_response() -> http::Response<Vec<u8>> {
614 http::Response::builder()
615 .status(404)
616 .body(b"Not Found".to_vec())
617 .expect("Failed to build not found response")
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use crate::ipc::{DecodedVariant, MessageType};
624 use std::sync::Arc;
625
626 fn ipc_message(message_type: MessageType) -> IPCMessage {
627 crate::ipc::empty_message(message_type)
628 }
629
630 fn handler_request(message_type: MessageType) -> http::Request<Vec<u8>> {
631 let engine = base64::engine::general_purpose::STANDARD;
632 let body_base64 = engine.encode(ipc_message(message_type).data());
633
634 http::Request::builder()
635 .uri("wry://index.html/__wbg__/handler")
636 .header("dioxus-data", body_base64)
637 .body(Vec::new())
638 .expect("failed to build request")
639 }
640
641 fn lock_request() -> http::Request<Vec<u8>> {
642 http::Request::builder()
643 .uri("wry://index.html/__wbg__/handler")
644 .header("wry-bindgen-lock", "1")
645 .body(Vec::new())
646 .expect("failed to build request")
647 }
648
649 fn initialized_request() -> http::Request<Vec<u8>> {
650 http::Request::builder()
651 .uri("wry://index.html/__wbg__/initialized")
652 .body(Vec::new())
653 .expect("failed to build request")
654 }
655
656 struct NoopWake;
657
658 impl std::task::Wake for NoopWake {
659 fn wake(self: Arc<Self>) {}
660 }
661
662 fn poll_forwarded_message(ipc: &WryIPC) -> IPCMessage {
663 let waker = std::task::Waker::from(Arc::new(NoopWake));
664 let mut cx = std::task::Context::from_waker(&waker);
665 match ipc.poll_recv(&mut cx) {
666 Poll::Ready(Some(Inbound::Message(msg))) => msg,
667 other => panic!("expected forwarded IPC message, got {other:?}"),
668 }
669 }
670
671 fn poll_driver(driver: &mut WryBindgenWebviewDriver) -> Poll<()> {
672 let waker = std::task::Waker::from(Arc::new(NoopWake));
673 let mut cx = std::task::Context::from_waker(&waker);
674 driver.poll(&mut cx)
675 }
676
677 #[test]
678 fn js_respond_is_forwarded_and_parks_xhr() {
679 let (ipc, sender, _driver_commands) = WryIPC::new();
680 let mut layer = WebviewMessageLayer::new(sender);
681 let responder_called = Rc::new(RefCell::new(false));
682 let captured_responder_called = responder_called.clone();
683
684 layer.receive_js_message(
685 ipc_message(MessageType::Respond),
686 WryBindgenResponder::from(move |_| {
687 *captured_responder_called.borrow_mut() = true;
688 }),
689 );
690
691 assert!(layer.current_xhr.is_some());
692 assert!(
693 !*responder_called.borrow(),
694 "JS response XHR should stay parked for Rust's next reply"
695 );
696 let received = poll_forwarded_message(&ipc);
697 assert!(matches!(
698 received.decoded().unwrap(),
699 DecodedVariant::Respond { .. }
700 ));
701 }
702
703 #[test]
704 fn js_message_while_xhr_is_parked_panics() {
705 let (_ipc, sender, _driver_commands) = WryIPC::new();
706 let mut layer = WebviewMessageLayer::new(sender);
707 layer.current_xhr = Some(WryBindgenResponder::from(|_| {}));
708
709 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
710 layer.receive_js_message(
711 ipc_message(MessageType::Evaluate),
712 WryBindgenResponder::from(|_| {}),
713 );
714 }));
715
716 assert!(result.is_err());
717 }
718
719 #[test]
720 fn lock_request_while_xhr_is_parked_panics() {
721 let (_ipc, sender, _driver_commands) = WryIPC::new();
722 let mut layer = WebviewMessageLayer::new(sender);
723 layer.current_xhr = Some(WryBindgenResponder::from(|_| {}));
724
725 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
726 layer.receive_lock_request(WryBindgenResponder::from(|_| {}));
727 }));
728
729 assert!(result.is_err());
730 }
731
732 #[test]
733 fn rust_outbound_messages_use_same_parked_xhr_response_path() {
734 for message_type in [MessageType::Evaluate, MessageType::Respond] {
735 let (_ipc, sender, _driver_commands) = WryIPC::new();
736 let mut layer = WebviewMessageLayer::new(sender);
737 let response = Rc::new(RefCell::new(None));
738 let captured_response = response.clone();
739 let message = ipc_message(message_type);
740 let expected_body = message.data().to_vec();
741
742 layer.current_xhr = Some(WryBindgenResponder::from(move |response| {
743 *captured_response.borrow_mut() = Some(response);
744 }));
745 layer.receive_rust_message(message);
746
747 assert!(layer.current_xhr.is_none());
748 let response = response
749 .borrow_mut()
750 .take()
751 .expect("parked XHR should receive Rust IPC");
752 assert_eq!(response.status(), http::StatusCode::OK);
753 let engine = base64::engine::general_purpose::STANDARD;
754 let body = engine
755 .decode(response.body())
756 .expect("response body should be base64 IPC bytes");
757 assert_eq!(body, expected_body);
758 }
759 }
760
761 #[test]
762 fn handler_responds_error_when_evaluate_arrives_after_runtime_drop() {
763 let bindgen = WryBindgen::new();
764 let protocol_handler = bindgen.protocol_handler();
765 drop(bindgen);
766
767 let response = Rc::new(RefCell::new(None));
768 let captured_response = response.clone();
769 let request = handler_request(MessageType::Evaluate);
770
771 let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
772 *captured_response.borrow_mut() = Some(response)
773 });
774
775 assert!(unhandled.is_none());
776 let response = response
777 .borrow_mut()
778 .take()
779 .expect("closed runtime should receive an error response");
780 assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
781 }
782
783 #[test]
784 fn lock_request_is_queued_until_webview_loads() {
785 let bindgen = WryBindgen::new();
786 let protocol_handler = bindgen.protocol_handler();
787
788 let evaluated_scripts = Rc::new(RefCell::new(Vec::new()));
789 let captured_scripts = evaluated_scripts.clone();
790 let (runtime, driver) = bindgen.split();
791 let mut driver = driver.with_evaluate_script(move |script| {
792 captured_scripts.borrow_mut().push(script.to_string());
793 });
794
795 runtime.ipc.send_acquire_lock();
796 assert!(matches!(poll_driver(&mut driver), Poll::Pending));
797 assert!(evaluated_scripts.borrow().is_empty());
798
799 let response = Rc::new(RefCell::new(None));
800 let captured_response = response.clone();
801 let request = initialized_request();
802 let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
803 *captured_response.borrow_mut() = Some(response)
804 });
805
806 assert!(unhandled.is_none());
807 assert_eq!(
808 response.borrow().as_ref().unwrap().status(),
809 http::StatusCode::OK
810 );
811 assert!(matches!(poll_driver(&mut driver), Poll::Pending));
812 assert_eq!(
813 evaluated_scripts.borrow().as_slice(),
814 ["window.__wry_acquire_handler_lock()"]
815 );
816 }
817
818 #[test]
819 fn lock_request_while_js_xhr_is_parked_is_not_dropped_or_duplicated() {
820 let bindgen = WryBindgen::new();
821 let protocol_handler = bindgen.protocol_handler();
822
823 let evaluated_scripts = Rc::new(RefCell::new(Vec::new()));
824 let captured_scripts = evaluated_scripts.clone();
825 let (runtime, driver) = bindgen.split();
826 let mut driver = driver.with_evaluate_script(move |script| {
827 captured_scripts.borrow_mut().push(script.to_string());
828 });
829
830 let request = initialized_request();
831 let unhandled = protocol_handler.handle_request("wry", &request, |_| {});
832 assert!(unhandled.is_none());
833
834 let response = Rc::new(RefCell::new(None));
835 let captured_response = response.clone();
836 let request = handler_request(MessageType::Evaluate);
837
838 let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
839 *captured_response.borrow_mut() = Some(response)
840 });
841
842 assert!(unhandled.is_none());
843 assert!(
844 response.borrow().is_none(),
845 "JS callback XHR should stay parked while Rust handles it"
846 );
847
848 runtime.ipc.send_acquire_lock();
849 assert!(matches!(poll_driver(&mut driver), Poll::Pending));
850 assert_eq!(
851 evaluated_scripts.borrow().as_slice(),
852 ["window.__wry_acquire_handler_lock()"],
853 "lock script should be requested while the parked XHR is outstanding"
854 );
855
856 runtime.ipc.send_ipc(ipc_message(MessageType::Respond));
857 assert!(matches!(poll_driver(&mut driver), Poll::Pending));
858
859 let response = response
860 .borrow_mut()
861 .take()
862 .expect("parked JS callback XHR should receive Rust's response");
863 assert_eq!(response.status(), http::StatusCode::OK);
864 assert_eq!(
865 evaluated_scripts.borrow().as_slice(),
866 ["window.__wry_acquire_handler_lock()"],
867 "answering the parked XHR should not duplicate the in-flight lock request"
868 );
869 }
870
871 #[test]
872 fn handler_responds_error_when_lock_arrives_after_runtime_drop() {
873 let bindgen = WryBindgen::new();
874 let protocol_handler = bindgen.protocol_handler();
875 drop(bindgen);
876
877 let response = Rc::new(RefCell::new(None));
878 let captured_response = response.clone();
879 let request = lock_request();
880
881 let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
882 *captured_response.borrow_mut() = Some(response)
883 });
884
885 assert!(unhandled.is_none());
886 let response = response
887 .borrow_mut()
888 .take()
889 .expect("closed runtime should receive an error response");
890 assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
891 }
892}