Add first Expectation to instruction

This commit is contained in:
Héctor Ramón Jiménez 2025-06-05 15:40:04 +02:00
parent f878b59977
commit d10b740545
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
5 changed files with 254 additions and 166 deletions

View file

@ -351,11 +351,12 @@ impl<P: Program + 'static> Tester<P> {
*outcome = Outcome::Failed; *outcome = Outcome::Failed;
} }
emulator::Event::Ready => { emulator::Event::Ready => {
*current += 1;
if let Some(instruction) = if let Some(instruction) =
self.instructions.get(*current).cloned() self.instructions.get(*current - 1).cloned()
{ {
emulator.run(program, instruction); emulator.run(program, instruction);
*current += 1;
} }
if *current >= self.instructions.len() { if *current >= self.instructions.len() {
@ -552,7 +553,7 @@ impl<P: Program + 'static> Tester<P> {
outcome, outcome,
.. ..
} => { } => {
if *current == i { if *current == i + 1 {
Some(match outcome { Some(match outcome {
Outcome::Running => { Outcome::Running => {
theme.palette().primary theme.palette().primary
@ -572,7 +573,7 @@ impl<P: Program + 'static> Tester<P> {
.color .color
} }
}) })
} else if *current > i { } else if *current > i + 1 {
Some( Some(
theme theme
.extended_palette() .extended_palette()

View file

@ -5,6 +5,7 @@ use crate::core::renderer;
use crate::core::widget; use crate::core::widget;
use crate::core::window; use crate::core::window;
use crate::core::{Element, Size}; use crate::core::{Element, Size};
use crate::instruction;
use crate::program; use crate::program;
use crate::program::Program; use crate::program::Program;
use crate::runtime::futures::futures::StreamExt; use crate::runtime::futures::futures::StreamExt;
@ -185,19 +186,41 @@ impl<P: Program + 'static> Emulator<P> {
&mut self.clipboard, &mut self.clipboard,
&mut messages, &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<P::Message>) { pub fn wait_for(&mut self, task: Task<P::Message>) {

View file

@ -1,6 +1,8 @@
use crate::Selector;
use crate::core::keyboard; use crate::core::keyboard;
use crate::core::mouse; use crate::core::mouse;
use crate::core::{Event, Point}; use crate::core::{Event, Point};
use crate::selector;
use crate::simulator; use crate::simulator;
use std::fmt; use std::fmt;
@ -8,6 +10,7 @@ use std::fmt;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Instruction { pub enum Instruction {
Interact(Interaction), Interact(Interaction),
Expect(Expectation),
} }
impl Instruction { impl Instruction {
@ -20,6 +23,7 @@ impl fmt::Display for Instruction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Instruction::Interact(interaction) => interaction.fmt(f), 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; pub use parser::Error as ParseError;
mod parser { mod parser {
@ -363,7 +385,11 @@ mod parser {
} }
fn instruction(input: &str) -> IResult<&str, Instruction> { 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> { fn interaction(input: &str) -> IResult<&str, Interaction> {
@ -414,6 +440,13 @@ mod parser {
.parse(input) .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> { fn key(input: &str) -> IResult<&str, Key> {
alt(( alt((
map(tag("enter"), |_| Key::Enter), map(tag("enter"), |_| Key::Enter),

View file

@ -1,6 +1,7 @@
//! Select widgets of a user interface. //! Select widgets of a user interface.
use crate::core::text; use crate::core::text;
use crate::core::widget; use crate::core::widget;
use crate::core::{Rectangle, Vector};
/// A selector describes a strategy to find a certain widget in a user interface. /// A selector describes a strategy to find a certain widget in a user interface.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -11,6 +12,165 @@ pub enum Selector {
Text(text::Fragment<'static>), Text(text::Fragment<'static>),
} }
impl Selector {
pub fn operation<'a>(&self) -> impl widget::Operation<Option<Target>> + 'a {
match self {
Selector::Id(id) => {
struct FindById {
id: widget::Id,
target: Option<Target>,
}
impl widget::Operation<Option<Target>> for FindById {
fn container(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<Option<Target>>,
),
) {
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<Option<Target>>
{
widget::operation::Outcome::Some(self.target)
}
}
Box::new(FindById {
id: id.clone(),
target: None,
}) as Box<dyn widget::Operation<_>>
}
Selector::Text(text) => {
struct FindByText {
text: text::Fragment<'static>,
target: Option<Target>,
}
impl widget::Operation<Option<Target>> for FindByText {
fn container(
&mut self,
_id: Option<&widget::Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<Option<Target>>,
),
) {
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<Option<Target>>
{
widget::operation::Outcome::Some(self.target)
}
}
Box::new(FindByText {
text: text.clone(),
target: None,
})
}
}
}
}
impl From<widget::Id> for Selector { impl From<widget::Id> for Selector {
fn from(id: widget::Id) -> Self { fn from(id: widget::Id) -> Self {
Self::Id(id) Self::Id(id)
@ -32,3 +192,10 @@ pub fn id(id: impl Into<widget::Id>) -> Selector {
pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector { pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector {
Selector::Text(fragment.into_fragment()) 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,
}

View file

@ -8,12 +8,11 @@ use crate::core::theme;
use crate::core::time; use crate::core::time;
use crate::core::widget; use crate::core::widget;
use crate::core::window; use crate::core::window;
use crate::core::{ use crate::core::{Element, Event, Font, Point, Settings, Size, SmolStr};
Element, Event, Font, Point, Rectangle, Settings, Size, SmolStr,
};
use crate::renderer; use crate::renderer;
use crate::runtime::UserInterface; use crate::runtime::UserInterface;
use crate::runtime::user_interface; use crate::runtime::user_interface;
use crate::selector;
use crate::{Error, Selector}; use crate::{Error, Selector};
use std::borrow::Cow; use std::borrow::Cow;
@ -36,13 +35,6 @@ pub struct Simulator<
messages: Vec<Message>, messages: Vec<Message>,
} }
/// 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> impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer>
where where
Theme: theme::Base, Theme: theme::Base,
@ -111,148 +103,20 @@ where
pub fn find( pub fn find(
&mut self, &mut self,
selector: impl Into<Selector>, selector: impl Into<Selector>,
) -> Result<Target, Error> { ) -> Result<selector::Target, Error> {
use widget::Operation;
let selector = selector.into(); let selector = selector.into();
let mut operation = selector.operation();
match &selector { self.raw.operate(
Selector::Id(id) => { &self.renderer,
struct FindById<'a> { &mut widget::operation::black_box(&mut operation),
id: &'a widget::Id, );
target: Option<Target>,
}
impl widget::Operation for FindById<'_> { match operation.finish() {
fn container( widget::operation::Outcome::Some(Some(target)) => Ok(target),
&mut self, _ => Err(Error::NotFound(selector)),
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<Target>,
}
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))
}
} }
} }
@ -271,7 +135,7 @@ where
pub fn click( pub fn click(
&mut self, &mut self,
selector: impl Into<Selector>, selector: impl Into<Selector>,
) -> Result<Target, Error> { ) -> Result<selector::Target, Error> {
let target = self.find(selector)?; let target = self.find(selector)?;
self.point_at(target.bounds.center()); self.point_at(target.bounds.center());