Introduce instruction::Target in test crate
This commit is contained in:
parent
f9755b0b7a
commit
bdcaadbe00
8 changed files with 150 additions and 82 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ edition = "2024"
|
|||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["tester"]
|
||||
test = ["iced/test"]
|
||||
tester = ["test", "iced/tester"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue