1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
//! Testing support for unit testing Crux apps.
use std::rc::Rc;
use anyhow::Result;
use crate::{
capability::{
channel::Receiver, executor_and_spawner, Operation, ProtoContext, QueuingExecutor,
},
Request, WithContext,
};
/// AppTester is a simplified execution environment for Crux apps for use in
/// tests.
///
/// Create an instance of `AppTester` with your `App` and an `Effect` type
/// using [`AppTester::default`].
///
/// for example:
///
/// ```rust,ignore
/// let app = AppTester::<ExampleApp, ExampleEffect>::default();
/// ```
pub struct AppTester<App, Ef>
where
App: crate::App,
{
app: App,
capabilities: App::Capabilities,
context: Rc<AppContext<Ef, App::Event>>,
}
struct AppContext<Ef, Ev> {
commands: Receiver<Ef>,
events: Receiver<Ev>,
executor: QueuingExecutor,
}
impl<App, Ef> AppTester<App, Ef>
where
App: crate::App,
{
/// Create an `AppTester` instance for an existing app instance. This can be used if your App
/// has a constructor other than `Default`, for example when used as a child app and expecting
/// configuration from the parent
pub fn new(app: App) -> Self
where
Ef: Send + 'static,
App::Capabilities: WithContext<App::Event, Ef>,
{
Self {
app,
..Default::default()
}
}
/// Run the app's `update` function with an event and a model state
///
/// You can use the resulting [`Update`] to inspect the effects which were requested
/// and potential further events dispatched by capabilities.
pub fn update(&self, event: App::Event, model: &mut App::Model) -> Update<Ef, App::Event> {
self.app.update(event, model, &self.capabilities);
self.context.updates()
}
/// Resolve an effect `request` from previous update with an operation output.
///
/// This potentially runs the app's `update` function if the effect is completed, and
/// produce another `Update`.
pub fn resolve<Op: Operation>(
&self,
request: &mut Request<Op>,
value: Op::Output,
) -> Result<Update<Ef, App::Event>> {
request.resolve(value)?;
Ok(self.context.updates())
}
/// Run the app's `view` function with a model state
pub fn view(&self, model: &App::Model) -> App::ViewModel {
self.app.view(model)
}
}
impl<App, Ef> Default for AppTester<App, Ef>
where
App: crate::App,
App::Capabilities: WithContext<App::Event, Ef>,
Ef: Send + 'static,
{
fn default() -> Self {
let (command_sender, commands) = crate::capability::channel();
let (event_sender, events) = crate::capability::channel();
let (executor, spawner) = executor_and_spawner();
let capability_context = ProtoContext::new(command_sender, event_sender, spawner);
Self {
app: App::default(),
capabilities: App::Capabilities::new_with_context(capability_context),
context: Rc::new(AppContext {
commands,
events,
executor,
}),
}
}
}
impl<App, Ef> AsRef<App::Capabilities> for AppTester<App, Ef>
where
App: crate::App,
{
fn as_ref(&self) -> &App::Capabilities {
&self.capabilities
}
}
impl<Ef, Ev> AppContext<Ef, Ev> {
pub fn updates(self: &Rc<Self>) -> Update<Ef, Ev> {
self.executor.run_all();
let effects = self.commands.drain().collect();
let events = self.events.drain().collect();
Update { effects, events }
}
}
/// Update test helper holds the result of running an app update using [`AppTester::update`]
/// or resolving a request with [`AppTester::resolve`].
#[derive(Debug)]
pub struct Update<Ef, Ev> {
/// Effects requested from the update run
pub effects: Vec<Ef>,
/// Events dispatched from the update run
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()
}
}
/// Panics if the pattern doesn't match an `Effect` from the specified `Update`
///
/// Like in a `match` expression, the pattern can be optionally followed by `if`
/// and a guard expression that has access to names bound by the pattern.
///
/// # Example
///
/// ```
/// # use crux_core::testing::Update;
/// # enum Effect { Render(String) };
/// # enum Event { None };
/// # let effects = vec![Effect::Render("test".to_string())].into_iter().collect();
/// # let mut update = Update { effects, events: vec!(Event::None) };
/// use crux_core::assert_effect;
/// assert_effect!(update, Effect::Render(_));
/// ```
#[macro_export]
macro_rules! assert_effect {
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
};
}