topcoat_runtime/
connection.rs1use std::any::TypeId;
2
3use topcoat_core::context::{Cx, try_request_context};
4use topcoat_view::{HoistKey, hoist_once};
5
6#[derive(Debug, Default, Clone, Copy)]
11pub(crate) struct ConnectedRender;
12
13#[must_use]
35#[track_caller]
36pub fn connected(cx: &Cx) -> bool {
37 hoist_once(HoistKey::new(TypeId::of::<ConnectedRender>()), |parts| {
38 parts.push_comment(|comment| {
39 comment.push_promoted_str_unescaped(&"::topcoat::connect");
40 });
41 });
42 connected_untracked(cx)
43}
44
45#[must_use]
51pub fn connected_untracked(cx: &Cx) -> bool {
52 try_request_context::<ConnectedRender>(cx).is_some()
53}
54
55#[cfg(test)]
56mod tests {
57 use std::{
58 panic::{AssertUnwindSafe, catch_unwind},
59 pin::pin,
60 task::{Context, Poll, Waker},
61 };
62
63 use topcoat::view::{HoistView, ViewExt, internal::ThenView, view};
64
65 use super::*;
66
67 const MARKER: &str = "<!--::topcoat::connect-->";
68
69 fn block_on<F: Future>(future: F) -> F::Output {
71 let mut future = pin!(future);
72 let mut cx = Context::from_waker(Waker::noop());
73 loop {
74 if let Poll::Ready(output) = future.as_mut().poll(&mut cx) {
75 return output;
76 }
77 }
78 }
79
80 fn render(cx: Cx, check: impl Fn(&Cx) -> bool + Send + 'static) -> String {
82 let view = HoistView::new(ThenView::new(async move {
83 let cx = &cx;
84 let result = check(cx);
85 Ok(view! { cx => <p>(result)</p> })
86 }));
87 block_on(view.single()).unwrap().render(&Cx::default())
88 }
89
90 #[test]
91 fn an_http_render_is_not_connected_and_requests_a_connection() {
92 let html = render(Cx::default(), connected);
93 assert!(html.starts_with(MARKER), "{html}");
94 assert!(html.ends_with("<p>false</p>"), "{html}");
95 }
96
97 #[test]
98 fn a_connected_render_is_connected_and_keeps_requesting_a_connection() {
99 let html = render(Cx::default().with(ConnectedRender), connected);
100 assert!(html.starts_with(MARKER), "{html}");
101 assert!(html.ends_with("<p>true</p>"), "{html}");
102 }
103
104 #[test]
105 fn repeated_checks_render_one_marker() {
106 let html = render(Cx::default(), |cx| {
107 let _ = connected(cx);
108 let _ = connected(cx);
109 connected(cx)
110 });
111 assert_eq!(html.matches(MARKER).count(), 1, "{html}");
112 }
113
114 #[test]
115 fn the_untracked_check_renders_no_marker() {
116 let html = render(Cx::default(), connected_untracked);
117 assert!(!html.contains("::topcoat::connect"), "{html}");
118 assert!(html.ends_with("<p>false</p>"), "{html}");
119
120 let html = render(Cx::default().with(ConnectedRender), connected_untracked);
121 assert!(!html.contains("::topcoat::connect"), "{html}");
122 assert!(html.ends_with("<p>true</p>"), "{html}");
123 }
124
125 #[test]
126 fn a_cloned_context_shares_the_render_mode() {
127 let cx = Cx::default().with(ConnectedRender);
128 assert!(connected_untracked(&cx.keyed(1)));
129 assert!(connected_untracked(&cx.with(())));
130 }
131
132 #[test]
133 fn the_tracked_check_outside_a_body_panics() {
134 let cx = Cx::default();
135 let panic = catch_unwind(AssertUnwindSafe(|| connected(&cx))).unwrap_err();
136 let message = panic.downcast::<&str>().expect("panics with a message");
137 assert!(message.contains("no view is collecting hoisted parts"));
138 }
139
140 #[test]
141 fn the_untracked_check_works_outside_a_body() {
142 assert!(!connected_untracked(&Cx::default()));
143 assert!(connected_untracked(&Cx::default().with(ConnectedRender)));
144 }
145}