use anyhow::Result;
use std::{collections::VecDeque, sync::Mutex};
use crate::{Command, Request, Resolvable, capability::Operation};
#[deprecated(
since = "0.17.0",
note = "AppTester has been deprecated and is left for backwards compatibility of test suites only. Apps can be tested without it using the Command API."
)]
pub struct AppTester<App>
where
App: crate::App,
{
app: App,
root_command: Mutex<Command<App::Effect, App::Event>>,
}
#[expect(deprecated)]
impl<App> AppTester<App>
where
App: crate::App + Default,
App::Model: Default,
{
pub fn new(app: App) -> Self {
Self {
app,
..Default::default()
}
}
pub fn update(
&self,
event: App::Event,
model: &mut App::Model,
) -> Update<App::Effect, App::Event> {
let command = self.app.update(event, model);
{
let mut root_command = self.root_command.lock().expect("AppTester mutex poisoned");
root_command.spawn(|ctx| command.into_future(ctx));
}
self.collect_update()
}
pub fn resolve<Output>(
&self,
request: &mut impl Resolvable<Output>,
value: Output,
) -> Result<Update<App::Effect, App::Event>> {
request.resolve(value)?;
Ok(self.collect_update())
}
#[track_caller]
pub fn resolve_to_event_then_update<Op: Operation>(
&self,
request: &mut Request<Op>,
value: Op::Output,
model: &mut App::Model,
) -> Update<App::Effect, App::Event> {
request.resolve(value).expect("failed to resolve request");
let event = self.collect_update().expect_one_event();
self.update(event, model)
}
pub fn view(&self, model: &App::Model) -> App::ViewModel {
self.app.view(model)
}
fn collect_update(&self) -> Update<App::Effect, App::Event> {
let mut root_command = self.root_command.lock().expect("AppTester mutex poisoned");
let effects: Vec<_> = root_command.effects().collect();
let events: Vec<_> = root_command.events().collect();
Update { effects, events }
}
}
#[expect(deprecated)]
impl<App> Default for AppTester<App>
where
App: crate::App + Default,
App::Model: Default,
{
fn default() -> Self {
Self {
app: App::default(),
root_command: Mutex::new(Command::done()),
}
}
}
#[derive(Debug)]
#[must_use]
pub struct Update<Ef, Ev> {
pub effects: Vec<Ef>,
pub events: Vec<Ev>,
}
impl<Ef, Ev> Update<Ef, Ev> {
pub fn into_effects(self) -> impl Iterator<Item = Ef> {
self.effects.into_iter()
}
pub fn effects(&self) -> impl Iterator<Item = &Ef> {
self.effects.iter()
}
pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
self.effects.iter_mut()
}
#[track_caller]
#[must_use]
pub fn expect_one_effect(mut self) -> Ef {
if self.events.is_empty() && self.effects.len() == 1 {
self.effects.pop().unwrap()
} else {
panic!(
"Expected one effect but found {} effect(s) and {} event(s)",
self.effects.len(),
self.events.len()
);
}
}
#[track_caller]
#[must_use]
pub fn expect_one_event(mut self) -> Ev {
if self.effects.is_empty() && self.events.len() == 1 {
self.events.pop().unwrap()
} else {
panic!(
"Expected one event but found {} effect(s) and {} event(s)",
self.effects.len(),
self.events.len()
);
}
}
#[track_caller]
pub fn assert_empty(self) {
if self.effects.is_empty() && self.events.is_empty() {
return;
}
panic!(
"Expected empty update but found {} effect(s) and {} event(s)",
self.effects.len(),
self.events.len()
);
}
pub fn take_effects<P>(&mut self, predicate: P) -> VecDeque<Ef>
where
P: FnMut(&Ef) -> bool,
{
let (matching_effects, other_effects) = self.take_effects_partitioned_by(predicate);
self.effects = other_effects.into_iter().collect();
matching_effects
}
pub fn take_effects_partitioned_by<P>(&mut self, predicate: P) -> (VecDeque<Ef>, VecDeque<Ef>)
where
P: FnMut(&Ef) -> bool,
{
std::mem::take(&mut self.effects)
.into_iter()
.partition(predicate)
}
}
impl<Effect, Event> Command<Effect, Event>
where
Effect: Send + 'static,
Event: Send + 'static,
{
#[track_caller]
pub fn expect_effect(&mut self) -> Effect {
let mut effects = self.effects();
if let Some(effect) = effects.next() {
return effect;
}
panic!("expected an effect but got none")
}
#[track_caller]
pub fn expect_one_effect(&mut self) -> Effect {
let mut effects = self.effects();
match (effects.next(), effects.next()) {
(None, _) => panic!("expected exactly one effect but got none"),
(Some(effect), None) => effect,
_ => panic!("expected exactly one effect but got more than one"),
}
}
#[track_caller]
pub fn expect_no_effects(&mut self) {
assert!(self.effects().next().is_none(), "expected no effects");
}
#[track_caller]
pub fn expect_no_events(&mut self) {
assert!(self.events().next().is_none(), "expected no events");
}
#[track_caller]
pub fn expect_event(&mut self) -> Event {
let mut event = self.events();
if let Some(event) = event.next() {
return event;
}
panic!("expected an event but got none")
}
#[track_caller]
pub fn expect_one_event(&mut self) -> Event {
let mut events = self.events();
match (events.next(), events.next()) {
(None, _) => panic!("expected exactly one event but got none"),
(Some(event), None) => event,
_ => panic!("expected exactly one effect but got more than one"),
}
}
#[track_caller]
pub fn expect_no_effect_or_events(&mut self) {
let effects = self.effects().count();
let events = self.events().count();
assert!(
effects + events == 0,
"expected command to be done, found {effects} effects and {events} events",
);
}
#[track_caller]
pub fn expect_done(&mut self) {
assert!(self.is_done(), "expected command to be done");
}
}
#[macro_export]
macro_rules! assert_effect {
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
};
}