From d10b74054546641f54407a975f2dbdaa50cb953e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Jun 2025 15:40:04 +0200 Subject: [PATCH] Add first `Expectation` to `instruction` --- devtools/src/tester.rs | 9 ++- test/src/emulator.rs | 45 ++++++++--- test/src/instruction.rs | 35 ++++++++- test/src/selector.rs | 167 ++++++++++++++++++++++++++++++++++++++++ test/src/simulator.rs | 164 ++++----------------------------------- 5 files changed, 254 insertions(+), 166 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index b7824dbc..9f8419e1 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -351,11 +351,12 @@ impl Tester

{ *outcome = Outcome::Failed; } emulator::Event::Ready => { + *current += 1; + if let Some(instruction) = - self.instructions.get(*current).cloned() + self.instructions.get(*current - 1).cloned() { emulator.run(program, instruction); - *current += 1; } if *current >= self.instructions.len() { @@ -552,7 +553,7 @@ impl Tester

{ outcome, .. } => { - if *current == i { + if *current == i + 1 { Some(match outcome { Outcome::Running => { theme.palette().primary @@ -572,7 +573,7 @@ impl Tester

{ .color } }) - } else if *current > i { + } else if *current > i + 1 { Some( theme .extended_palette() diff --git a/test/src/emulator.rs b/test/src/emulator.rs index fa1c4b3d..cc557f37 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -5,6 +5,7 @@ use crate::core::renderer; use crate::core::widget; use crate::core::window; use crate::core::{Element, Size}; +use crate::instruction; use crate::program; use crate::program::Program; use crate::runtime::futures::futures::StreamExt; @@ -185,19 +186,41 @@ impl Emulator

{ &mut self.clipboard, &mut messages, ); + + self.cache = Some(user_interface.into_cache()); + + let task = + Task::batch(messages.into_iter().map(|message| { + program.update(&mut self.state, message) + })); + + self.wait_for(task); + self.resubscribe(program); } + Instruction::Expect(expectation) => match expectation { + instruction::Expectation::Presence(selector) => { + use widget::Operation; + + let mut operation = selector.operation(); + + user_interface.operate( + &self.renderer, + &mut widget::operation::black_box(&mut operation), + ); + + match operation.finish() { + widget::operation::Outcome::Some(Some(_)) => { + self.runtime.send(Event::Ready); + } + _ => { + self.runtime.send(Event::Failed); + } + } + + self.cache = Some(user_interface.into_cache()); + } + }, } - - self.cache = Some(user_interface.into_cache()); - - let task = Task::batch( - messages - .into_iter() - .map(|message| program.update(&mut self.state, message)), - ); - - self.wait_for(task); - self.resubscribe(program); } pub fn wait_for(&mut self, task: Task) { diff --git a/test/src/instruction.rs b/test/src/instruction.rs index fd9ba2c9..bdfa0d48 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -1,6 +1,8 @@ +use crate::Selector; use crate::core::keyboard; use crate::core::mouse; use crate::core::{Event, Point}; +use crate::selector; use crate::simulator; use std::fmt; @@ -8,6 +10,7 @@ use std::fmt; #[derive(Debug, Clone)] pub enum Instruction { Interact(Interaction), + Expect(Expectation), } impl Instruction { @@ -20,6 +23,7 @@ impl fmt::Display for Instruction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Instruction::Interact(interaction) => interaction.fmt(f), + Instruction::Expect(expectation) => expectation.fmt(f), } } } @@ -337,6 +341,24 @@ mod format { } } +#[derive(Debug, Clone)] +pub enum Expectation { + Presence(Selector), +} + +impl fmt::Display for Expectation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Expectation::Presence(Selector::Id(_id)) => { + write!(f, "expect id") // TODO + } + Expectation::Presence(Selector::Text(text)) => { + write!(f, "expect text \"{text}\"") + } + } + } +} + pub use parser::Error as ParseError; mod parser { @@ -363,7 +385,11 @@ mod parser { } fn instruction(input: &str) -> IResult<&str, Instruction> { - map(interaction, Instruction::Interact).parse(input) + alt(( + map(interaction, Instruction::Interact), + map(expectation, Instruction::Expect), + )) + .parse(input) } fn interaction(input: &str) -> IResult<&str, Interaction> { @@ -414,6 +440,13 @@ mod parser { .parse(input) } + fn expectation(input: &str) -> IResult<&str, Expectation> { + map(preceded(tag("expect text "), string), |text| { + Expectation::Presence(selector::text(text)) + }) + .parse(input) + } + fn key(input: &str) -> IResult<&str, Key> { alt(( map(tag("enter"), |_| Key::Enter), diff --git a/test/src/selector.rs b/test/src/selector.rs index 58e0fca4..b6a5b294 100644 --- a/test/src/selector.rs +++ b/test/src/selector.rs @@ -1,6 +1,7 @@ //! Select widgets of a user interface. use crate::core::text; use crate::core::widget; +use crate::core::{Rectangle, Vector}; /// A selector describes a strategy to find a certain widget in a user interface. #[derive(Debug, Clone, PartialEq, Eq)] @@ -11,6 +12,165 @@ pub enum Selector { Text(text::Fragment<'static>), } +impl Selector { + pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { + match self { + Selector::Id(id) => { + struct FindById { + id: widget::Id, + target: Option, + } + + impl widget::Operation> for FindById { + fn container( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation>, + ), + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + return; + } + + operate_on_children(self); + } + + fn scrollable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _content_bounds: Rectangle, + _translation: Vector, + _state: &mut dyn widget::operation::Scrollable, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text_input( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn widget::operation::TextInput, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _text: &str, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn custom( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn std::any::Any, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn finish( + &self, + ) -> widget::operation::Outcome> + { + widget::operation::Outcome::Some(self.target) + } + } + + Box::new(FindById { + id: id.clone(), + target: None, + }) as Box> + } + Selector::Text(text) => { + struct FindByText { + text: text::Fragment<'static>, + target: Option, + } + + impl widget::Operation> for FindByText { + fn container( + &mut self, + _id: Option<&widget::Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation>, + ), + ) { + if self.target.is_some() { + return; + } + + operate_on_children(self); + } + + fn text( + &mut self, + _id: Option<&widget::Id>, + bounds: Rectangle, + text: &str, + ) { + if self.target.is_some() { + return; + } + + if self.text == text { + self.target = Some(Target { bounds }); + } + } + + fn finish( + &self, + ) -> widget::operation::Outcome> + { + widget::operation::Outcome::Some(self.target) + } + } + + Box::new(FindByText { + text: text.clone(), + target: None, + }) + } + } + } +} + impl From for Selector { fn from(id: widget::Id) -> Self { Self::Id(id) @@ -32,3 +192,10 @@ pub fn id(id: impl Into) -> Selector { pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector { Selector::Text(fragment.into_fragment()) } + +/// A specific area, normally containing a widget. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Target { + /// The bounds of the area. + pub bounds: Rectangle, +} diff --git a/test/src/simulator.rs b/test/src/simulator.rs index cdff5040..c5505d5b 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -8,12 +8,11 @@ use crate::core::theme; use crate::core::time; use crate::core::widget; use crate::core::window; -use crate::core::{ - Element, Event, Font, Point, Rectangle, Settings, Size, SmolStr, -}; +use crate::core::{Element, Event, Font, Point, Settings, Size, SmolStr}; use crate::renderer; use crate::runtime::UserInterface; use crate::runtime::user_interface; +use crate::selector; use crate::{Error, Selector}; use std::borrow::Cow; @@ -36,13 +35,6 @@ pub struct Simulator< messages: Vec, } -/// A specific area of a [`Simulator`], normally containing a widget. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Target { - /// The bounds of the area. - pub bounds: Rectangle, -} - impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer> where Theme: theme::Base, @@ -111,148 +103,20 @@ where pub fn find( &mut self, selector: impl Into, - ) -> Result { + ) -> Result { + use widget::Operation; + let selector = selector.into(); + let mut operation = selector.operation(); - match &selector { - Selector::Id(id) => { - struct FindById<'a> { - id: &'a widget::Id, - target: Option, - } + self.raw.operate( + &self.renderer, + &mut widget::operation::black_box(&mut operation), + ); - impl widget::Operation for FindById<'_> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<()>, - ), - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - return; - } - - operate_on_children(self); - } - - fn scrollable( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - _translation: core::Vector, - _state: &mut dyn widget::operation::Scrollable, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text_input( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn widget::operation::TextInput, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _text: &str, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn custom( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn std::any::Any, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - } - - let mut find = FindById { id, target: None }; - self.raw.operate(&self.renderer, &mut find); - - find.target.ok_or(Error::NotFound(selector)) - } - Selector::Text(text) => { - struct FindByText<'a> { - text: &'a str, - target: Option, - } - - impl widget::Operation for FindByText<'_> { - fn container( - &mut self, - _id: Option<&widget::Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<()>, - ), - ) { - if self.target.is_some() { - return; - } - - operate_on_children(self); - } - - fn text( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - text: &str, - ) { - if self.target.is_some() { - return; - } - - if self.text == text { - self.target = Some(Target { bounds }); - } - } - } - - let mut find = FindByText { text, target: None }; - self.raw.operate(&self.renderer, &mut find); - - find.target.ok_or(Error::NotFound(selector)) - } + match operation.finish() { + widget::operation::Outcome::Some(Some(target)) => Ok(target), + _ => Err(Error::NotFound(selector)), } } @@ -271,7 +135,7 @@ where pub fn click( &mut self, selector: impl Into, - ) -> Result { + ) -> Result { let target = self.find(selector)?; self.point_at(target.bounds.center());