cranpose_testing/
testing.rs1use cranpose_core::{
2 location_key, ApplierGuard, Composition, Key, MemoryApplier, NodeError, NodeId, RuntimeHandle,
3 ROOT_RENDER_REPLAY_LIMIT,
4};
5use cranpose_ui::{request_render_invalidation, reset_render_state_for_tests, AppContext};
6
7#[cfg(test)]
8use cranpose_core::{
9 pop_parent, push_parent, with_current_composer, with_node_mut, MutableState, Node,
10};
11#[cfg(test)]
12use std::cell::Cell;
13#[cfg(test)]
14use std::rc::Rc;
15use std::rc::Rc as StdRc;
16
17pub struct ComposeTestRule {
24 _scope: cranpose_ui::AppContextScope,
25 app_context: StdRc<AppContext>,
26 composition: Composition<MemoryApplier>,
27 content: Option<Box<dyn FnMut()>>, initial_root_key: Key,
29}
30
31impl ComposeTestRule {
32 pub fn new() -> Self {
34 let app_context = AppContext::new();
35 app_context.enter(reset_render_state_for_tests);
36 let scope = app_context.enter_scope();
37 Self {
38 _scope: scope,
39 app_context,
40 composition: Composition::new(MemoryApplier::new()),
41 content: None,
42 initial_root_key: location_key(file!(), line!(), column!()),
43 }
44 }
45
46 fn root_key(&self) -> Key {
47 self.composition.root_key().unwrap_or(self.initial_root_key)
48 }
49
50 pub fn set_content(&mut self, content: impl FnMut() + 'static) -> Result<(), NodeError> {
53 self.content = Some(Box::new(content));
54 self.render()
55 }
56
57 pub fn recomposition(&mut self) -> Result<(), NodeError> {
59 self.render()
60 }
61
62 pub fn advance_frame(&mut self, frame_time_nanos: u64) -> Result<(), NodeError> {
65 let app_context = StdRc::clone(&self.app_context);
66 app_context.enter(|| {
67 let handle = self.composition.runtime_handle();
68 handle.drain_frame_callbacks(frame_time_nanos);
69 self.pump_until_idle_in_context()
70 })
71 }
72
73 pub fn pump_until_idle(&mut self) -> Result<(), NodeError> {
76 let app_context = StdRc::clone(&self.app_context);
77 app_context.enter(|| self.pump_until_idle_in_context())
78 }
79
80 fn pump_until_idle_in_context(&mut self) -> Result<(), NodeError> {
81 let mut i = 0;
82 loop {
83 let mut progressed = false;
84 i += 1;
85 if i > ROOT_RENDER_REPLAY_LIMIT {
86 return Err(NodeError::RecompositionLimitExceeded {
87 operation: "pump_until_idle",
88 limit: ROOT_RENDER_REPLAY_LIMIT,
89 });
90 }
91
92 if self.composition.should_render() {
93 self.render()?;
94 progressed = true;
95 }
96
97 let handle = self.composition.runtime_handle();
98 if handle.has_updates() {
99 self.composition.flush_pending_node_updates()?;
100 progressed = true;
101 }
102
103 if handle.has_invalid_scopes() {
104 let changed = self.composition.process_invalid_scopes()?;
105 if changed {
106 request_render_invalidation();
108 }
109 if self.composition.take_root_render_request() {
110 self.render()?;
111 }
112 progressed = true;
113 }
114
115 if !progressed {
116 break;
117 }
118 }
119 Ok(())
120 }
121
122 pub fn runtime_handle(&self) -> RuntimeHandle {
125 self.composition.runtime_handle()
126 }
127
128 pub fn applier_mut(&mut self) -> ApplierGuard<'_, MemoryApplier> {
131 self.composition.applier_mut()
132 }
133
134 pub fn dump_tree(&mut self) -> String {
136 let root = self.composition.root();
137 let applier = self.composition.applier_mut();
138 applier.dump_tree(root)
139 }
140
141 pub fn has_content(&self) -> bool {
143 self.content.is_some()
144 }
145
146 pub fn root_id(&self) -> Option<NodeId> {
148 self.composition.root()
149 }
150
151 pub fn with_app_context<R>(&self, block: impl FnOnce() -> R) -> R {
152 self.app_context.enter(block)
153 }
154
155 pub fn composition(&mut self) -> &mut Composition<MemoryApplier> {
157 &mut self.composition
158 }
159
160 pub fn placed_semantics(
171 &mut self,
172 size: cranpose_ui::Size,
173 ) -> Result<Option<crate::placed_semantics::PlacedSemanticsNode>, NodeError> {
174 self.pump_until_idle()?;
175 let Some(root) = self.composition.root() else {
176 return Ok(None);
177 };
178 let handle = self.composition.runtime_handle();
179 let app_context = StdRc::clone(&self.app_context);
180 app_context.enter(|| {
181 let mut applier = self.composition.applier_mut();
182 applier.set_runtime_handle(handle);
183 let placed =
184 crate::placed_semantics::placed_semantics_from_applier(&mut applier, root, size);
185 applier.clear_runtime_handle();
186 placed
187 })
188 }
189
190 fn render(&mut self) -> Result<(), NodeError> {
191 let app_context = StdRc::clone(&self.app_context);
192 app_context.enter(|| {
193 let key = self.root_key();
194 if let Some(content) = self.content.as_mut() {
195 self.composition.render(key, &mut **content)?;
196 self.drain_root_render_requests()?;
197 request_render_invalidation();
200 }
201 Ok(())
202 })
203 }
204
205 fn drain_root_render_requests(&mut self) -> Result<(), NodeError> {
206 let key = self.root_key();
207 for _ in 0..ROOT_RENDER_REPLAY_LIMIT {
208 if !self.composition.take_root_render_request() {
209 return Ok(());
210 }
211 let content = self
212 .content
213 .as_mut()
214 .ok_or(NodeError::RecompositionLimitExceeded {
215 operation: "root render replay",
216 limit: ROOT_RENDER_REPLAY_LIMIT,
217 })?;
218 self.composition.render(key, &mut **content)?;
219 request_render_invalidation();
220 }
221
222 Err(NodeError::RecompositionLimitExceeded {
223 operation: "root render replay",
224 limit: ROOT_RENDER_REPLAY_LIMIT,
225 })
226 }
227}
228
229impl Default for ComposeTestRule {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235pub fn run_test_composition<R>(f: impl FnOnce(&mut ComposeTestRule) -> R) -> R {
238 let mut rule = ComposeTestRule::new();
239 f(&mut rule)
240}
241
242#[cfg(test)]
243#[path = "tests/testing_tests.rs"]
244mod tests;
245
246#[cfg(test)]
247#[path = "tests/recomposition_tests.rs"]
248mod recomposition_tests;