1use anyhow::Result;
3use std::{collections::VecDeque, sync::Arc};
4
5#[expect(deprecated)]
6use crate::WithContext;
7use crate::{
8 Command, Request, Resolvable,
9 capability::{
10 CommandSpawner, Operation, ProtoContext, QueuingExecutor, channel::Receiver,
11 executor_and_spawner,
12 },
13};
14
15pub struct AppTester<App>
36where
37 App: crate::App,
38{
39 app: App,
40 capabilities: App::Capabilities,
41 context: Arc<AppContext<App::Effect, App::Event>>,
42 command_spawner: CommandSpawner<App::Effect, App::Event>,
43}
44
45struct AppContext<Ef, Ev> {
46 commands: Receiver<Ef>,
47 events: Receiver<Ev>,
48 executor: QueuingExecutor,
49}
50
51#[expect(deprecated)]
52impl<App> AppTester<App>
53where
54 App: crate::App,
55{
56 pub fn new(app: App) -> Self
60 where
61 App::Capabilities: WithContext<App::Event, App::Effect>,
62 {
63 Self {
64 app,
65 ..Default::default()
66 }
67 }
68
69 pub fn update(
74 &self,
75 event: App::Event,
76 model: &mut App::Model,
77 ) -> Update<App::Effect, App::Event> {
78 let command = self.app.update(event, model, &self.capabilities);
79 self.command_spawner.spawn(command);
80
81 self.context.updates()
82 }
83
84 pub fn resolve<Output>(
93 &self,
94 request: &mut impl Resolvable<Output>,
95 value: Output,
96 ) -> Result<Update<App::Effect, App::Event>> {
97 request.resolve(value)?;
98
99 Ok(self.context.updates())
100 }
101
102 #[track_caller]
111 pub fn resolve_to_event_then_update<Op: Operation>(
112 &self,
113 request: &mut Request<Op>,
114 value: Op::Output,
115 model: &mut App::Model,
116 ) -> Update<App::Effect, App::Event> {
117 request.resolve(value).expect("failed to resolve request");
118 let event = self.context.updates().expect_one_event();
119 self.update(event, model)
120 }
121
122 pub fn view(&self, model: &App::Model) -> App::ViewModel {
124 self.app.view(model)
125 }
126}
127
128#[expect(deprecated)]
129impl<App> Default for AppTester<App>
130where
131 App: crate::App,
132 App::Capabilities: WithContext<App::Event, App::Effect>,
133{
134 fn default() -> Self {
135 let (command_sender, commands) = crate::capability::channel();
136 let (event_sender, events) = crate::capability::channel();
137 let (executor, spawner) = executor_and_spawner();
138 let capability_context = ProtoContext::new(command_sender, event_sender, spawner);
139 let command_spawner = CommandSpawner::new(capability_context.clone());
140
141 Self {
142 app: App::default(),
143 capabilities: App::Capabilities::new_with_context(capability_context),
144 context: Arc::new(AppContext {
145 commands,
146 events,
147 executor,
148 }),
149 command_spawner,
150 }
151 }
152}
153
154impl<App> AsRef<App::Capabilities> for AppTester<App>
155where
156 App: crate::App,
157{
158 fn as_ref(&self) -> &App::Capabilities {
159 &self.capabilities
160 }
161}
162
163impl<Ef, Ev> AppContext<Ef, Ev> {
164 pub fn updates(self: &Arc<Self>) -> Update<Ef, Ev> {
165 self.executor.run_all();
166 let effects = self.commands.drain().collect();
167 let events = self.events.drain().collect();
168
169 Update { effects, events }
170 }
171}
172
173#[derive(Debug)]
176#[must_use]
177pub struct Update<Ef, Ev> {
178 pub effects: Vec<Ef>,
180 pub events: Vec<Ev>,
182}
183
184impl<Ef, Ev> Update<Ef, Ev> {
185 pub fn into_effects(self) -> impl Iterator<Item = Ef> {
186 self.effects.into_iter()
187 }
188
189 pub fn effects(&self) -> impl Iterator<Item = &Ef> {
190 self.effects.iter()
191 }
192
193 pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
194 self.effects.iter_mut()
195 }
196
197 #[track_caller]
203 #[must_use]
204 pub fn expect_one_effect(mut self) -> Ef {
205 if self.events.is_empty() && self.effects.len() == 1 {
206 self.effects.pop().unwrap()
207 } else {
208 panic!(
209 "Expected one effect but found {} effect(s) and {} event(s)",
210 self.effects.len(),
211 self.events.len()
212 );
213 }
214 }
215
216 #[track_caller]
222 #[must_use]
223 pub fn expect_one_event(mut self) -> Ev {
224 if self.effects.is_empty() && self.events.len() == 1 {
225 self.events.pop().unwrap()
226 } else {
227 panic!(
228 "Expected one event but found {} effect(s) and {} event(s)",
229 self.effects.len(),
230 self.events.len()
231 );
232 }
233 }
234
235 #[track_caller]
240 pub fn assert_empty(self) {
241 if self.effects.is_empty() && self.events.is_empty() {
242 return;
243 }
244 panic!(
245 "Expected empty update but found {} effect(s) and {} event(s)",
246 self.effects.len(),
247 self.events.len()
248 );
249 }
250
251 pub fn take_effects<P>(&mut self, predicate: P) -> VecDeque<Ef>
254 where
255 P: FnMut(&Ef) -> bool,
256 {
257 let (matching_effects, other_effects) = self.take_effects_partitioned_by(predicate);
258
259 self.effects = other_effects.into_iter().collect();
260
261 matching_effects
262 }
263
264 pub fn take_effects_partitioned_by<P>(&mut self, predicate: P) -> (VecDeque<Ef>, VecDeque<Ef>)
267 where
268 P: FnMut(&Ef) -> bool,
269 {
270 std::mem::take(&mut self.effects)
271 .into_iter()
272 .partition(predicate)
273 }
274}
275
276impl<Effect, Event> Command<Effect, Event>
277where
278 Effect: Send + 'static,
279 Event: Send + 'static,
280{
281 #[track_caller]
287 pub fn expect_one_effect(&mut self) -> Effect {
288 assert!(
289 self.events().next().is_none(),
290 "expected only one effect, but found an event"
291 );
292 let mut effects = self.effects();
293 match (effects.next(), effects.next()) {
294 (None, _) => panic!("expected one effect but got none"),
295 (Some(effect), None) => effect,
296 _ => panic!("expected one effect but got more than one"),
297 }
298 }
299}
300
301#[macro_export]
318macro_rules! assert_effect {
319 ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
320 assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
321 };
322}