Write documentation for new iced_test APIs

This commit is contained in:
Héctor Ramón Jiménez 2025-09-12 22:53:28 +02:00
parent 5796ba272e
commit 59e2687146
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
6 changed files with 240 additions and 21 deletions

View file

@ -1,3 +1,4 @@
//! Run your application in a headless runtime.
use crate::core;
use crate::core::mouse;
use crate::core::renderer;
@ -21,6 +22,15 @@ use crate::{Instruction, Selector};
use std::fmt;
/// A headless runtime that can run iced applications and execute
/// [instructions](crate::Instruction).
///
/// An [`Emulator`] runs its program as close as possible to the real thing.
/// It will run subscriptions and tasks in the [`Executor`](Program::Executor) of
/// the [`Program`].
///
/// If you want to run a simulation without side effects, use a [`Simulator`](crate::Simulator)
/// instead.
pub struct Emulator<P: Program> {
state: P::State,
runtime: Runtime<P::Executor, mpsc::Sender<Event<P>>, Event<P>>,
@ -34,18 +44,30 @@ pub struct Emulator<P: Program> {
pending_tasks: usize,
}
/// An emulation event.
pub enum Event<P: Program> {
/// An action that must be [performed](Emulator::perform) by the [`Emulator`].
Action(Action<P>),
/// An [`Instruction`] failed to be executed.
Failed(Instruction),
/// The [`Emulator`] is ready.
Ready,
}
pub enum Action<P: Program> {
/// An action that must be [performed](Emulator::perform) by the [`Emulator`].
pub struct Action<P: Program>(Action_<P>);
enum Action_<P: Program> {
Runtime(runtime::Action<P::Message>),
CountDown,
}
impl<P: Program + 'static> Emulator<P> {
/// Creates a new [`Emulator`] of the [`Program`] with the given [`Mode`] and [`Size`].
///
/// The [`Emulator`] will send [`Event`] notifications through the provided [`mpsc::Sender`].
///
/// When the [`Emulator`] has finished booting, an [`Event::Ready`] will be produced.
pub fn new(
sender: mpsc::Sender<Event<P>>,
program: &P,
@ -55,6 +77,10 @@ impl<P: Program + 'static> Emulator<P> {
Self::with_preset(sender, program, mode, size, None)
}
/// Creates a new [`Emulator`] analogously to [`new`](Self::new), but it also takes a
/// [`program::Preset`] that will be used as the initial state.
///
/// When the [`Emulator`] has finished booting, an [`Event::Ready`] will be produced.
pub fn with_preset(
sender: mpsc::Sender<Event<P>>,
program: &P,
@ -106,6 +132,11 @@ impl<P: Program + 'static> Emulator<P> {
emulator
}
/// Updates the state of the [`Emulator`] program.
///
/// This is equivalent to calling the [`Program::update`] function,
/// resubscribing to any subscriptions, and running the resulting tasks
/// concurrently.
pub fn update(&mut self, program: &P, message: P::Message) {
let task = self
.runtime
@ -118,16 +149,24 @@ impl<P: Program + 'static> Emulator<P> {
_ => {
if let Some(stream) = task::into_stream(task) {
self.runtime.run(
stream.map(Action::Runtime).map(Event::Action).boxed(),
stream
.map(Action_::Runtime)
.map(Action)
.map(Event::Action)
.boxed(),
);
}
}
}
}
/// Performs an [`Action`].
///
/// Whenever an [`Emulator`] sends an [`Event::Action`], this
/// method must be called to proceed with the execution.
pub fn perform(&mut self, program: &P, action: Action<P>) {
match action {
Action::CountDown => {
match action.0 {
Action_::CountDown => {
if self.pending_tasks > 0 {
self.pending_tasks -= 1;
@ -136,7 +175,7 @@ impl<P: Program + 'static> Emulator<P> {
}
}
}
Action::Runtime(action) => match action {
Action_::Runtime(action) => match action {
runtime::Action::Output(message) => {
self.update(program, message);
}
@ -229,6 +268,12 @@ impl<P: Program + 'static> Emulator<P> {
}
}
/// Runs an [`Instruction`].
///
/// If the [`Instruction`] executes successfully, an [`Event::Ready`] will be
/// produced by the [`Emulator`].
///
/// Otherwise, an [`Event::Failed`] will be triggered.
pub fn run(&mut self, program: &P, instruction: Instruction) {
let mut user_interface = UserInterface::build(
program.view(&self.state, self.window),
@ -321,7 +366,7 @@ impl<P: Program + 'static> Emulator<P> {
}
}
pub fn wait_for(&mut self, task: Task<P::Message>) {
fn wait_for(&mut self, task: Task<P::Message>) {
if let Some(stream) = task::into_stream(task) {
match self.mode {
Mode::Zen => {
@ -329,10 +374,11 @@ impl<P: Program + 'static> Emulator<P> {
self.runtime.run(
stream
.map(Action::Runtime)
.map(Action_::Runtime)
.map(Action)
.map(Event::Action)
.chain(stream::once(async {
Event::Action(Action::CountDown)
Event::Action(Action(Action_::CountDown))
}))
.boxed(),
);
@ -340,7 +386,8 @@ impl<P: Program + 'static> Emulator<P> {
Mode::Patient => {
self.runtime.run(
stream
.map(Action::Runtime)
.map(Action_::Runtime)
.map(Action)
.map(Event::Action)
.chain(stream::once(async { Event::Ready }))
.boxed(),
@ -348,7 +395,11 @@ impl<P: Program + 'static> Emulator<P> {
}
Mode::Impatient => {
self.runtime.run(
stream.map(Action::Runtime).map(Event::Action).boxed(),
stream
.map(Action_::Runtime)
.map(Action)
.map(Event::Action)
.boxed(),
);
self.runtime.send(Event::Ready);
}
@ -358,17 +409,18 @@ impl<P: Program + 'static> Emulator<P> {
}
}
pub fn resubscribe(&mut self, program: &P) {
fn resubscribe(&mut self, program: &P) {
self.runtime
.track(subscription::into_recipes(self.runtime.enter(|| {
program.subscription(&self.state).map(|message| {
Event::Action(Action::Runtime(runtime::Action::Output(
message,
Event::Action(Action(Action_::Runtime(
runtime::Action::Output(message),
)))
})
})));
}
/// Returns the current view of the [`Emulator`].
pub fn view(
&self,
program: &P,
@ -376,24 +428,37 @@ impl<P: Program + 'static> Emulator<P> {
program.view(&self.state, self.window)
}
/// Returns the current theme of the [`Emulator`].
pub fn theme(&self, program: &P) -> Option<P::Theme> {
program.theme(&self.state, self.window)
}
/// Turns the [`Emulator`] into its internal state.
pub fn into_state(self) -> (P::State, core::window::Id) {
(self.state, self.window)
}
}
/// The strategy used by an [`Emulator`] when waiting for tasks to finish.
///
/// A [`Mode`] can be used to make an [`Emulator`] wait for side effects to finish before
/// continuing execution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
/// Waits for all tasks spawned by an [`Instruction`], as well as all tasks indirectly
/// spawned by the the results of those tasks.
///
/// This is the default.
#[default]
Zen,
/// Waits only for the tasks directly spawned by an [`Instruction`].
Patient,
/// Never waits for any tasks to finish.
Impatient,
}
impl Mode {
/// A list of all the available modes.
pub const ALL: &[Self] = &[Self::Zen, Self::Patient, Self::Impatient];
}