Introduce instruction::Target in test crate

This commit is contained in:
Héctor Ramón Jiménez 2025-08-20 13:47:34 +02:00
parent f9755b0b7a
commit bdcaadbe00
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
8 changed files with 150 additions and 82 deletions

View file

@ -5,12 +5,20 @@ use crate::widget::operation::Operation;
/// The internal state of a widget that has text input. /// The internal state of a widget that has text input.
pub trait TextInput { pub trait TextInput {
/// Returns the current _visible_ text of the text input
///
/// Normally, this is either its value or its placeholder.
fn text(&self) -> &str;
/// Moves the cursor of the text input to the front of the input text. /// Moves the cursor of the text input to the front of the input text.
fn move_cursor_to_front(&mut self); fn move_cursor_to_front(&mut self);
/// Moves the cursor of the text input to the end of the input text. /// Moves the cursor of the text input to the end of the input text.
fn move_cursor_to_end(&mut self); fn move_cursor_to_end(&mut self);
/// Moves the cursor of the text input to an arbitrary location. /// Moves the cursor of the text input to an arbitrary location.
fn move_cursor_to(&mut self, position: usize); fn move_cursor_to(&mut self, position: usize);
/// Selects all the content of the text input. /// Selects all the content of the text input.
fn select_all(&mut self); fn select_all(&mut self);
} }

View file

@ -6,6 +6,7 @@ edition = "2024"
publish = false publish = false
[features] [features]
default = ["tester"]
test = ["iced/test"] test = ["iced/test"]
tester = ["test", "iced/tester"] tester = ["test", "iced/tester"]

View file

@ -2,13 +2,11 @@ viewport: 512x768
mode: impatient mode: impatient
preset: Empty preset: Empty
----- -----
click left at (377.80, 236.50) click left at "What needs to be done?"
type "Create the universe" type "Create the universe"
type enter type enter
type "Make an apple pie" type "Make an apple pie"
type enter type enter
click left at (135.40, 351.70) click left at "Create the universe"
click left at (153.80, 398.10) click left at "Make an apple pie"
move cursor to (511.40, 448.50) expect "0 tasks left"
expect text "Create the universe"
expect text "Make an apple pie"

View file

@ -16,6 +16,7 @@ use crate::runtime::task;
use crate::runtime::user_interface; use crate::runtime::user_interface;
use crate::runtime::window; use crate::runtime::window;
use crate::runtime::{Action, Task, UserInterface}; use crate::runtime::{Action, Task, UserInterface};
use crate::selector;
use std::fmt; use std::fmt;
@ -212,7 +213,34 @@ impl<P: Program + 'static> Emulator<P> {
match instruction { match instruction {
Instruction::Interact(interaction) => { Instruction::Interact(interaction) => {
let events = interaction.events(); let Some(events) = interaction.events(|target| match target {
instruction::Target::Point(position) => Some(*position),
instruction::Target::Text(text) => {
use widget::Operation;
let mut operation =
selector::text(text.to_owned()).operation();
user_interface.operate(
&self.renderer,
&mut widget::operation::black_box(&mut operation),
);
match operation.finish() {
widget::operation::Outcome::Some(matches) => {
matches
.first()
.copied()
.map(|target| target.bounds.center())
}
_ => None,
}
}
}) else {
self.runtime.send(Event::Failed);
self.cache = Some(user_interface.into_cache());
return;
};
for event in &events { for event in &events {
if let core::Event::Mouse(mouse::Event::CursorMoved { if let core::Event::Mouse(mouse::Event::CursorMoved {
@ -242,10 +270,10 @@ impl<P: Program + 'static> Emulator<P> {
self.resubscribe(program); self.resubscribe(program);
} }
Instruction::Expect(expectation) => match expectation { Instruction::Expect(expectation) => match expectation {
instruction::Expectation::Presence(selector) => { instruction::Expectation::Text(text) => {
use widget::Operation; use widget::Operation;
let mut operation = selector.operation(); let mut operation = selector::text(text).operation();
user_interface.operate( user_interface.operate(
&self.renderer, &self.renderer,
@ -253,7 +281,9 @@ impl<P: Program + 'static> Emulator<P> {
); );
match operation.finish() { match operation.finish() {
widget::operation::Outcome::Some(Some(_)) => { widget::operation::Outcome::Some(matches)
if matches.len() == 1 =>
{
self.runtime.send(Event::Ready); self.runtime.send(Event::Ready);
} }
_ => { _ => {

View file

@ -1,8 +1,6 @@
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;
@ -38,7 +36,9 @@ impl Interaction {
pub fn from_event(event: Event) -> Option<Self> { pub fn from_event(event: Event) -> Option<Self> {
Some(match event { Some(match event {
Event::Mouse(mouse) => Self::Mouse(match mouse { Event::Mouse(mouse) => Self::Mouse(match mouse {
mouse::Event::CursorMoved { position } => Mouse::Move(position), mouse::Event::CursorMoved { position } => {
Mouse::Move(Target::Point(position))
}
mouse::Event::ButtonPressed(button) => { mouse::Event::ButtonPressed(button) => {
Mouse::Press { button, at: None } Mouse::Press { button, at: None }
} }
@ -115,8 +115,8 @@ impl Interaction {
at: release_at, at: release_at,
}, },
) if press == release ) if press == release
&& release_at.is_none_or(|release_at| { && release_at.as_ref().is_none_or(|release_at| {
Some(release_at) == press_at Some(release_at) == press_at.as_ref()
}) => }) =>
{ {
( (
@ -127,22 +127,6 @@ impl Interaction {
None, None,
) )
} }
(
current @ Mouse::Release {
button: button_a,
at: at_a,
}
| current @ Mouse::Click {
button: button_a,
at: at_a,
},
Mouse::Release {
button: button_b,
at: at_b,
},
) if button_a == button_b && at_a == at_b => {
(Self::Mouse(current), None)
}
(current, next) => { (current, next) => {
(Self::Mouse(current), Some(Self::Mouse(next))) (Self::Mouse(current), Some(Self::Mouse(next)))
} }
@ -173,7 +157,10 @@ impl Interaction {
} }
} }
pub fn events(&self) -> Vec<Event> { pub fn events(
&self,
find_target: impl FnOnce(&Target) -> Option<Point>,
) -> Option<Vec<Event>> {
let mouse_move_ = let mouse_move_ =
|to| Event::Mouse(mouse::Event::CursorMoved { position: to }); |to| Event::Mouse(mouse::Event::CursorMoved { position: to });
@ -187,20 +174,22 @@ impl Interaction {
let key_release = |key| simulator::release_key(key); let key_release = |key| simulator::release_key(key);
match self { Some(match self {
Interaction::Mouse(mouse) => match mouse { Interaction::Mouse(mouse) => match mouse {
Mouse::Move(to) => vec![mouse_move_(*to)], Mouse::Move(to) => vec![mouse_move_(find_target(to)?)],
Mouse::Press { Mouse::Press {
button, button,
at: Some(at), at: Some(at),
} => vec![mouse_move_(*at), mouse_press(*button)], } => vec![mouse_move_(find_target(at)?), mouse_press(*button)],
Mouse::Press { button, at: None } => { Mouse::Press { button, at: None } => {
vec![mouse_press(*button)] vec![mouse_press(*button)]
} }
Mouse::Release { Mouse::Release {
button, button,
at: Some(at), at: Some(at),
} => vec![mouse_move_(*at), mouse_release(*button)], } => {
vec![mouse_move_(find_target(at)?), mouse_release(*button)]
}
Mouse::Release { button, at: None } => { Mouse::Release { button, at: None } => {
vec![mouse_release(*button)] vec![mouse_release(*button)]
} }
@ -209,7 +198,7 @@ impl Interaction {
at: Some(at), at: Some(at),
} => { } => {
vec![ vec![
mouse_move_(*at), mouse_move_(find_target(at)?),
mouse_press(*button), mouse_press(*button),
mouse_release(*button), mouse_release(*button),
] ]
@ -226,7 +215,7 @@ impl Interaction {
simulator::typewrite(text).collect() simulator::typewrite(text).collect()
} }
}, },
} })
} }
} }
@ -241,40 +230,55 @@ impl fmt::Display for Interaction {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Mouse { pub enum Mouse {
Move(Point), Move(Target),
Press { Press {
button: mouse::Button, button: mouse::Button,
at: Option<Point>, at: Option<Target>,
}, },
Release { Release {
button: mouse::Button, button: mouse::Button,
at: Option<Point>, at: Option<Target>,
}, },
Click { Click {
button: mouse::Button, button: mouse::Button,
at: Option<Point>, at: Option<Target>,
}, },
} }
impl fmt::Display for Mouse { impl fmt::Display for Mouse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Mouse::Move(point) => { Mouse::Move(target) => {
write!(f, "move cursor to ({:.2}, {:.2})", point.x, point.y) write!(f, "move cursor to {}", target)
} }
Mouse::Press { button, at } => { Mouse::Press { button, at } => {
write!(f, "press {}", format::button_at(*button, *at)) write!(f, "press {}", format::button_at(*button, at.as_ref()))
} }
Mouse::Release { button, at } => { Mouse::Release { button, at } => {
write!(f, "release {}", format::button_at(*button, *at)) write!(f, "release {}", format::button_at(*button, at.as_ref()))
} }
Mouse::Click { button, at } => { Mouse::Click { button, at } => {
write!(f, "click {}", format::button_at(*button, *at)) write!(f, "click {}", format::button_at(*button, at.as_ref()))
} }
} }
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum Target {
Point(Point),
Text(String),
}
impl fmt::Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Point(point) => f.write_str(&format::point(*point)),
Self::Text(text) => f.write_str(&format::string(text)),
}
}
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Keyboard { pub enum Keyboard {
Press(Key), Press(Key),
@ -324,9 +328,9 @@ impl From<Key> for keyboard::Key {
mod format { mod format {
use super::*; use super::*;
pub fn button_at(button: mouse::Button, at: Option<Point>) -> String { pub fn button_at(button: mouse::Button, at: Option<&Target>) -> String {
if let Some(at) = at { if let Some(at) = at {
format!("{} at {}", self::button(button), point(at)) format!("{} at {}", self::button(button), at)
} else { } else {
self::button(button).to_owned() self::button(button).to_owned()
} }
@ -355,21 +359,22 @@ mod format {
Key::Backspace => "backspace", Key::Backspace => "backspace",
} }
} }
pub fn string(text: &str) -> String {
format!("\"{}\"", text.escape_default())
}
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Expectation { pub enum Expectation {
Presence(Selector), Text(String),
} }
impl fmt::Display for Expectation { impl fmt::Display for Expectation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Expectation::Presence(Selector::Id(_id)) => { Expectation::Text(text) => {
write!(f, "expect id") // TODO write!(f, "expect {}", format::string(text))
}
Expectation::Presence(Selector::Text(text)) => {
write!(f, "expect text \"{text}\"")
} }
} }
} }
@ -383,7 +388,7 @@ mod parser {
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::character::complete::{char, multispace0, satisfy}; use nom::character::complete::{char, multispace0, satisfy};
use nom::combinator::{map, opt, recognize}; use nom::combinator::{cut, map, opt, recognize};
use nom::multi::many0; use nom::multi::many0;
use nom::number::float; use nom::number::float;
use nom::sequence::{delimited, preceded, separated_pair}; use nom::sequence::{delimited, preceded, separated_pair};
@ -418,7 +423,7 @@ mod parser {
fn mouse(input: &str) -> IResult<&str, Mouse> { fn mouse(input: &str) -> IResult<&str, Mouse> {
let mouse_move = let mouse_move =
preceded(tag("move cursor to "), point).map(Mouse::Move); preceded(tag("move cursor to "), target).map(Mouse::Move);
alt((mouse_move, mouse_click)).parse(input) alt((mouse_move, mouse_click)).parse(input)
} }
@ -433,13 +438,21 @@ mod parser {
fn mouse_button_at( fn mouse_button_at(
input: &str, input: &str,
) -> IResult<&str, (mouse::Button, Option<Point>)> { ) -> IResult<&str, (mouse::Button, Option<Target>)> {
let (input, button) = mouse_button(input)?; let (input, button) = mouse_button(input)?;
let (input, at) = opt(preceded(tag(" at "), point)).parse(input)?; let (input, at) = opt(target).parse(input)?;
Ok((input, (button, at))) Ok((input, (button, at)))
} }
fn target(input: &str) -> IResult<&str, Target> {
preceded(
tag(" at "),
cut(alt((string.map(Target::Text), point.map(Target::Point)))),
)
.parse(input)
}
fn mouse_button(input: &str) -> IResult<&str, mouse::Button> { fn mouse_button(input: &str) -> IResult<&str, mouse::Button> {
alt(( alt((
tag("left").map(|_| mouse::Button::Left), tag("left").map(|_| mouse::Button::Left),
@ -457,8 +470,8 @@ mod parser {
} }
fn expectation(input: &str) -> IResult<&str, Expectation> { fn expectation(input: &str) -> IResult<&str, Expectation> {
map(preceded(tag("expect text "), string), |text| { map(preceded(tag("expect "), string), |text| {
Expectation::Presence(selector::text(text)) Expectation::Text(text)
}) })
.parse(input) .parse(input)
} }
@ -474,6 +487,7 @@ mod parser {
} }
fn string(input: &str) -> IResult<&str, String> { fn string(input: &str) -> IResult<&str, String> {
// TODO: Proper string literal parsing
delimited( delimited(
char('"'), char('"'),
map(recognize(many0(satisfy(|c| c != '"'))), str::to_owned), map(recognize(many0(satisfy(|c| c != '"'))), str::to_owned),

View file

@ -13,7 +13,7 @@ pub enum Selector {
} }
impl Selector { impl Selector {
pub fn operation<'a>(&self) -> impl widget::Operation<Option<Target>> + 'a { pub fn operation<'a>(&self) -> impl widget::Operation<Vec<Target>> + 'a {
match self { match self {
Selector::Id(id) => { Selector::Id(id) => {
struct FindById { struct FindById {
@ -21,13 +21,13 @@ impl Selector {
target: Option<Target>, target: Option<Target>,
} }
impl widget::Operation<Option<Target>> for FindById { impl widget::Operation<Vec<Target>> for FindById {
fn container( fn container(
&mut self, &mut self,
id: Option<&widget::Id>, id: Option<&widget::Id>,
bounds: Rectangle, bounds: Rectangle,
operate_on_children: &mut dyn FnMut( operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<Option<Target>>, &mut dyn widget::Operation<Vec<Target>>,
), ),
) { ) {
if self.target.is_some() { if self.target.is_some() {
@ -106,9 +106,13 @@ impl Selector {
fn finish( fn finish(
&self, &self,
) -> widget::operation::Outcome<Option<Target>> ) -> widget::operation::Outcome<Vec<Target>>
{ {
widget::operation::Outcome::Some(self.target) if let Some(target) = self.target {
widget::operation::Outcome::Some(vec![target])
} else {
widget::operation::Outcome::None
}
} }
} }
@ -120,51 +124,54 @@ impl Selector {
Selector::Text(text) => { Selector::Text(text) => {
struct FindByText { struct FindByText {
text: text::Fragment<'static>, text: text::Fragment<'static>,
target: Option<Target>, target: Vec<Target>,
} }
impl widget::Operation<Option<Target>> for FindByText { impl widget::Operation<Vec<Target>> for FindByText {
fn container( fn container(
&mut self, &mut self,
_id: Option<&widget::Id>, _id: Option<&widget::Id>,
_bounds: Rectangle, _bounds: Rectangle,
operate_on_children: &mut dyn FnMut( operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<Option<Target>>, &mut dyn widget::Operation<Vec<Target>>,
), ),
) { ) {
if self.target.is_some() {
return;
}
operate_on_children(self); operate_on_children(self);
} }
fn text_input(
&mut self,
_id: Option<&widget::Id>,
bounds: Rectangle,
state: &mut dyn widget::operation::TextInput,
) {
if self.text == state.text() {
self.target.push(Target { bounds });
}
}
fn text( fn text(
&mut self, &mut self,
_id: Option<&widget::Id>, _id: Option<&widget::Id>,
bounds: Rectangle, bounds: Rectangle,
text: &str, text: &str,
) { ) {
if self.target.is_some() {
return;
}
if self.text == text { if self.text == text {
self.target = Some(Target { bounds }); self.target.push(Target { bounds });
} }
} }
fn finish( fn finish(
&self, &self,
) -> widget::operation::Outcome<Option<Target>> ) -> widget::operation::Outcome<Vec<Target>>
{ {
widget::operation::Outcome::Some(self.target) widget::operation::Outcome::Some(self.target.clone())
} }
} }
Box::new(FindByText { Box::new(FindByText {
text: text.clone(), text: text.clone(),
target: None, target: Vec::new(),
}) })
} }
} }

View file

@ -115,7 +115,9 @@ where
); );
match operation.finish() { match operation.finish() {
widget::operation::Outcome::Some(Some(target)) => Ok(target), widget::operation::Outcome::Some(matches) => {
matches.first().copied().ok_or(Error::NotFound(selector))
}
_ => Err(Error::NotFound(selector)), _ => Err(Error::NotFound(selector)),
} }
} }

View file

@ -1631,6 +1631,14 @@ impl<P: text::Paragraph> operation::Focusable for State<P> {
} }
impl<P: text::Paragraph> operation::TextInput for State<P> { impl<P: text::Paragraph> operation::TextInput for State<P> {
fn text(&self) -> &str {
if self.value.content().is_empty() {
self.placeholder.content()
} else {
self.value.content()
}
}
fn move_cursor_to_front(&mut self) { fn move_cursor_to_front(&mut self) {
State::move_cursor_to_front(self); State::move_cursor_to_front(self);
} }