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.
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.
fn move_cursor_to_front(&mut self);
/// Moves the cursor of the text input to the end of the input text.
fn move_cursor_to_end(&mut self);
/// Moves the cursor of the text input to an arbitrary location.
fn move_cursor_to(&mut self, position: usize);
/// Selects all the content of the text input.
fn select_all(&mut self);
}

View file

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

View file

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

View file

@ -16,6 +16,7 @@ use crate::runtime::task;
use crate::runtime::user_interface;
use crate::runtime::window;
use crate::runtime::{Action, Task, UserInterface};
use crate::selector;
use std::fmt;
@ -212,7 +213,34 @@ impl<P: Program + 'static> Emulator<P> {
match instruction {
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 {
if let core::Event::Mouse(mouse::Event::CursorMoved {
@ -242,10 +270,10 @@ impl<P: Program + 'static> Emulator<P> {
self.resubscribe(program);
}
Instruction::Expect(expectation) => match expectation {
instruction::Expectation::Presence(selector) => {
instruction::Expectation::Text(text) => {
use widget::Operation;
let mut operation = selector.operation();
let mut operation = selector::text(text).operation();
user_interface.operate(
&self.renderer,
@ -253,7 +281,9 @@ impl<P: Program + 'static> Emulator<P> {
);
match operation.finish() {
widget::operation::Outcome::Some(Some(_)) => {
widget::operation::Outcome::Some(matches)
if matches.len() == 1 =>
{
self.runtime.send(Event::Ready);
}
_ => {

View file

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

View file

@ -13,7 +13,7 @@ pub enum 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 {
Selector::Id(id) => {
struct FindById {
@ -21,13 +21,13 @@ impl Selector {
target: Option<Target>,
}
impl widget::Operation<Option<Target>> for FindById {
impl widget::Operation<Vec<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>>,
&mut dyn widget::Operation<Vec<Target>>,
),
) {
if self.target.is_some() {
@ -106,9 +106,13 @@ impl Selector {
fn finish(
&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) => {
struct FindByText {
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(
&mut self,
_id: Option<&widget::Id>,
_bounds: Rectangle,
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);
}
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(
&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 });
self.target.push(Target { bounds });
}
}
fn finish(
&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 {
text: text.clone(),
target: None,
target: Vec::new(),
})
}
}

View file

@ -115,7 +115,9 @@ where
);
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)),
}
}

View file

@ -1631,6 +1631,14 @@ impl<P: text::Paragraph> operation::Focusable 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) {
State::move_cursor_to_front(self);
}