1use anyhow::Result;
3use std::{collections::VecDeque, sync::Arc};
4
5use crate::{
6 capability::{
7 channel::Receiver, executor_and_spawner, CommandSpawner, Operation, ProtoContext,
8 QueuingExecutor,
9 },
10 Command, Request, WithContext,
11};
12
13pub struct AppTester<App>
34where
35 App: crate::App,
36{
37 app: App,
38 capabilities: App::Capabilities,
39 context: Arc<AppContext<App::Effect, App::Event>>,
40 command_spawner: CommandSpawner<App::Effect, App::Event>,
41}
42
43struct AppContext<Ef, Ev> {
44 commands: Receiver<Ef>,
45 events: Receiver<Ev>,
46 executor: QueuingExecutor,
47}
48
49impl<App> AppTester<App>
50where
51 App: crate::App,
52{
53 pub fn new(app: App) -> Self
57 where
58 App::Capabilities: WithContext<App::Event, App::Effect>,
59 {
60 Self {
61 app,
62 ..Default::default()
63 }
64 }
65
66 pub fn update(
71 &self,
72 event: App::Event,
73 model: &mut App::Model,
74 ) -> Update<App::Effect, App::Event> {
75 let command = self.app.update(event, model, &self.capabilities);
76 self.command_spawner.spawn(command);
77
78 self.context.updates()
79 }
80
81 pub fn resolve<Op: Operation>(
86 &self,
87 request: &mut Request<Op>,
88 value: Op::Output,
89 ) -> Result<Update<App::Effect, App::Event>> {
90 request.resolve(value)?;
91
92 Ok(self.context.updates())
93 }
94
95 pub fn resolve_to_event_then_update<Op: Operation>(
100 &self,
101 request: &mut Request<Op>,
102 value: Op::Output,
103 model: &mut App::Model,
104 ) -> Update<App::Effect, App::Event> {
105 request.resolve(value).expect("failed to resolve request");
106 let event = self.context.updates().expect_one_event();
107 self.update(event, model)
108 }
109
110 pub fn view(&self, model: &App::Model) -> App::ViewModel {
112 self.app.view(model)
113 }
114}
115
116impl<App> Default for AppTester<App>
117where
118 App: crate::App,
119 App::Capabilities: WithContext<App::Event, App::Effect>,
120{
121 fn default() -> Self {
122 let (command_sender, commands) = crate::capability::channel();
123 let (event_sender, events) = crate::capability::channel();
124 let (executor, spawner) = executor_and_spawner();
125 let capability_context = ProtoContext::new(command_sender, event_sender, spawner);
126 let command_spawner = CommandSpawner::new(capability_context.clone());
127
128 Self {
129 app: App::default(),
130 capabilities: App::Capabilities::new_with_context(capability_context),
131 context: Arc::new(AppContext {
132 commands,
133 events,
134 executor,
135 }),
136 command_spawner,
137 }
138 }
139}
140
141impl<App> AsRef<App::Capabilities> for AppTester<App>
142where
143 App: crate::App,
144{
145 fn as_ref(&self) -> &App::Capabilities {
146 &self.capabilities
147 }
148}
149
150impl<Ef, Ev> AppContext<Ef, Ev> {
151 pub fn updates(self: &Arc<Self>) -> Update<Ef, Ev> {
152 self.executor.run_all();
153 let effects = self.commands.drain().collect();
154 let events = self.events.drain().collect();
155
156 Update { effects, events }
157 }
158}
159
160#[derive(Debug)]
163#[must_use]
164pub struct Update<Ef, Ev> {
165 pub effects: Vec<Ef>,
167 pub events: Vec<Ev>,
169}
170
171impl<Ef, Ev> Update<Ef, Ev> {
172 pub fn into_effects(self) -> impl Iterator<Item = Ef> {
173 self.effects.into_iter()
174 }
175
176 pub fn effects(&self) -> impl Iterator<Item = &Ef> {
177 self.effects.iter()
178 }
179
180 pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
181 self.effects.iter_mut()
182 }
183
184 #[track_caller]
187 pub fn expect_one_effect(mut self) -> Ef {
188 if self.events.is_empty() && self.effects.len() == 1 {
189 self.effects.pop().unwrap()
190 } else {
191 panic!(
192 "Expected one effect but found {} effect(s) and {} event(s)",
193 self.effects.len(),
194 self.events.len()
195 );
196 }
197 }
198
199 #[track_caller]
202 pub fn expect_one_event(mut self) -> Ev {
203 if self.effects.is_empty() && self.events.len() == 1 {
204 self.events.pop().unwrap()
205 } else {
206 panic!(
207 "Expected one event but found {} effect(s) and {} event(s)",
208 self.effects.len(),
209 self.events.len()
210 );
211 }
212 }
213
214 #[track_caller]
216 pub fn assert_empty(self) {
217 if self.effects.is_empty() && self.events.is_empty() {
218 return;
219 }
220 panic!(
221 "Expected empty update but found {} effect(s) and {} event(s)",
222 self.effects.len(),
223 self.events.len()
224 );
225 }
226
227 pub fn take_effects<P>(&mut self, predicate: P) -> VecDeque<Ef>
230 where
231 P: FnMut(&Ef) -> bool,
232 {
233 let (matching_effects, other_effects) = self.take_effects_partitioned_by(predicate);
234
235 self.effects = other_effects.into_iter().collect();
236
237 matching_effects
238 }
239
240 pub fn take_effects_partitioned_by<P>(&mut self, predicate: P) -> (VecDeque<Ef>, VecDeque<Ef>)
243 where
244 P: FnMut(&Ef) -> bool,
245 {
246 std::mem::take(&mut self.effects)
247 .into_iter()
248 .partition(predicate)
249 }
250}
251
252impl<Effect, Event> Command<Effect, Event>
253where
254 Effect: Send + 'static,
255 Event: Send + 'static,
256{
257 #[track_caller]
260 pub fn expect_one_effect(&mut self) -> Effect {
261 if self.events().next().is_some() {
262 panic!("Expected only one effect, but found an event");
263 }
264 if let Some(effect) = self.effects().next() {
265 effect
266 } else {
267 panic!("Expected one effect but found none");
268 }
269 }
270}
271
272#[macro_export]
289macro_rules! assert_effect {
290 ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
291 assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
292 };
293}