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

@ -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)),
}
}