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>(
90 &self,
91 request: &mut Request<Op>,
92 value: Op::Output,
93 ) -> Result<Update<App::Effect, App::Event>> {
94 request.resolve(value)?;
95
96 Ok(self.context.updates())
97 }
98
99 #[track_caller]
108 pub fn resolve_to_event_then_update<Op: Operation>(
109 &self,
110 request: &mut Request<Op>,
111 value: Op::Output,
112 model: &mut App::Model,
113 ) -> Update<App::Effect, App::Event> {
114 request.resolve(value).expect("failed to resolve request");
115 let event = self.context.updates().expect_one_event();
116 self.update(event, model)
117 }
118
119 pub fn view(&self, model: &App::Model) -> App::ViewModel {
121 self.app.view(model)
122 }
123}
124
125impl<App> Default for AppTester<App>
126where
127 App: crate::App,
128 App::Capabilities: WithContext<App::Event, App::Effect>,
129{
130 fn default() -> Self {
131 let (command_sender, commands) = crate::capability::channel();
132 let (event_sender, events) = crate::capability::channel();
133 let (executor, spawner) = executor_and_spawner();
134 let capability_context = ProtoContext::new(command_sender, event_sender, spawner);
135 let command_spawner = CommandSpawner::new(capability_context.clone());
136
137 Self {
138 app: App::default(),
139 capabilities: App::Capabilities::new_with_context(capability_context),
140 context: Arc::new(AppContext {
141 commands,
142 events,
143 executor,
144 }),
145 command_spawner,
146 }
147 }
148}
149
150impl<App> AsRef<App::Capabilities> for AppTester<App>
151where
152 App: crate::App,
153{
154 fn as_ref(&self) -> &App::Capabilities {
155 &self.capabilities
156 }
157}
158
159impl<Ef, Ev> AppContext<Ef, Ev> {
160 pub fn updates(self: &Arc<Self>) -> Update<Ef, Ev> {
161 self.executor.run_all();
162 let effects = self.commands.drain().collect();
163 let events = self.events.drain().collect();
164
165 Update { effects, events }
166 }
167}
168
169#[derive(Debug)]
172#[must_use]
173pub struct Update<Ef, Ev> {
174 pub effects: Vec<Ef>,
176 pub events: Vec<Ev>,
178}
179
180impl<Ef, Ev> Update<Ef, Ev> {
181 pub fn into_effects(self) -> impl Iterator<Item = Ef> {
182 self.effects.into_iter()
183 }
184
185 pub fn effects(&self) -> impl Iterator<Item = &Ef> {
186 self.effects.iter()
187 }
188
189 pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
190 self.effects.iter_mut()
191 }
192
193 #[track_caller]
199 #[must_use]
200 pub fn expect_one_effect(mut self) -> Ef {
201 if self.events.is_empty() && self.effects.len() == 1 {
202 self.effects.pop().unwrap()
203 } else {
204 panic!(
205 "Expected one effect but found {} effect(s) and {} event(s)",
206 self.effects.len(),
207 self.events.len()
208 );
209 }
210 }
211
212 #[track_caller]
218 #[must_use]
219 pub fn expect_one_event(mut self) -> Ev {
220 if self.effects.is_empty() && self.events.len() == 1 {
221 self.events.pop().unwrap()
222 } else {
223 panic!(
224 "Expected one event but found {} effect(s) and {} event(s)",
225 self.effects.len(),
226 self.events.len()
227 );
228 }
229 }
230
231 #[track_caller]
236 pub fn assert_empty(self) {
237 if self.effects.is_empty() && self.events.is_empty() {
238 return;
239 }
240 panic!(
241 "Expected empty update but found {} effect(s) and {} event(s)",
242 self.effects.len(),
243 self.events.len()
244 );
245 }
246
247 pub fn take_effects<P>(&mut self, predicate: P) -> VecDeque<Ef>
250 where
251 P: FnMut(&Ef) -> bool,
252 {
253 let (matching_effects, other_effects) = self.take_effects_partitioned_by(predicate);
254
255 self.effects = other_effects.into_iter().collect();
256
257 matching_effects
258 }
259
260 pub fn take_effects_partitioned_by<P>(&mut self, predicate: P) -> (VecDeque<Ef>, VecDeque<Ef>)
263 where
264 P: FnMut(&Ef) -> bool,
265 {
266 std::mem::take(&mut self.effects)
267 .into_iter()
268 .partition(predicate)
269 }
270}
271
272impl<Effect, Event> Command<Effect, Event>
273where
274 Effect: Send + 'static,
275 Event: Send + 'static,
276{
277 #[track_caller]
283 pub fn expect_one_effect(&mut self) -> Effect {
284 assert!(
285 self.events().next().is_none(),
286 "expected only one effect, but found an event"
287 );
288 let mut effects = self.effects();
289 match (effects.next(), effects.next()) {
290 (None, _) => panic!("expected one effect but got none"),
291 (Some(effect), None) => effect,
292 _ => panic!("expected one effect but got more than one"),
293 }
294 }
295}
296
297#[macro_export]
314macro_rules! assert_effect {
315 ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
316 assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
317 };
318}