Draft iced_test crate and test todos example
This commit is contained in:
parent
d6182299b9
commit
d09d5d45ae
18 changed files with 640 additions and 63 deletions
|
|
@ -72,6 +72,7 @@ unconditional-rendering = ["iced_winit/unconditional-rendering"]
|
|||
iced_core.workspace = true
|
||||
iced_futures.workspace = true
|
||||
iced_renderer.workspace = true
|
||||
iced_test.workspace = true
|
||||
iced_widget.workspace = true
|
||||
iced_winit.features = ["program"]
|
||||
iced_winit.workspace = true
|
||||
|
|
@ -111,6 +112,7 @@ members = [
|
|||
"highlighter",
|
||||
"renderer",
|
||||
"runtime",
|
||||
"test",
|
||||
"tiny_skia",
|
||||
"wgpu",
|
||||
"widget",
|
||||
|
|
@ -137,6 +139,7 @@ iced_graphics = { version = "0.14.0-dev", path = "graphics" }
|
|||
iced_highlighter = { version = "0.14.0-dev", path = "highlighter" }
|
||||
iced_renderer = { version = "0.14.0-dev", path = "renderer" }
|
||||
iced_runtime = { version = "0.14.0-dev", path = "runtime" }
|
||||
iced_test = { version = "0.14.0-dev", path = "test" }
|
||||
iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" }
|
||||
iced_wgpu = { version = "0.14.0-dev", path = "wgpu" }
|
||||
iced_widget = { version = "0.14.0-dev", path = "widget" }
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ impl Key {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Named> for Key {
|
||||
fn from(named: Named) -> Self {
|
||||
Self::Named(named)
|
||||
}
|
||||
}
|
||||
|
||||
/// A named key.
|
||||
///
|
||||
/// This is mostly the `NamedKey` type found in [`winit`].
|
||||
|
|
|
|||
|
|
@ -30,24 +30,45 @@ pub trait Operation<T = ()>: Send {
|
|||
);
|
||||
|
||||
/// Operates on a widget that can be focused.
|
||||
fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn Focusable,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that can be scrolled.
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_state: &mut dyn Scrollable,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
_state: &mut dyn Scrollable,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that has text input.
|
||||
fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
|
||||
fn text_input(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn TextInput,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that contains some text.
|
||||
fn text(&mut self, _id: Option<&Id>, _bounds: Rectangle, _text: &str) {}
|
||||
|
||||
/// Operates on a custom widget with some state.
|
||||
fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {}
|
||||
fn custom(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn Any,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Finishes the [`Operation`] and returns its [`Outcome`].
|
||||
fn finish(&self) -> Outcome<T> {
|
||||
|
|
@ -68,33 +89,52 @@ where
|
|||
self.as_mut().container(id, bounds, operate_on_children);
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.as_mut().focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.as_mut().focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.as_mut().scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.as_mut().text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.as_mut().text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.as_mut().custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.as_mut().text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.as_mut().custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<O> {
|
||||
|
|
@ -150,33 +190,52 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.operation.focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.operation.text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<O> {
|
||||
|
|
@ -234,39 +293,55 @@ where
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn Focusable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn TextInput,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(state, id);
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
text: &str,
|
||||
) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,33 +350,52 @@ where
|
|||
MapRef { operation }.container(id, bounds, operate_on_children);
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.operation.focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.operation.text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<B> {
|
||||
|
|
@ -361,33 +455,52 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.operation.focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: crate::Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.operation.text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<B> {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for Focus {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.focus();
|
||||
|
|
@ -64,7 +69,12 @@ pub fn count() -> impl Operation<Count> {
|
|||
}
|
||||
|
||||
impl Operation<Count> for CountFocusable {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if state.is_focused() {
|
||||
self.count.focused = Some(self.count.total);
|
||||
}
|
||||
|
|
@ -104,7 +114,12 @@ where
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for FocusPrevious {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if self.count.total == 0 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -147,7 +162,12 @@ where
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for FocusNext {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
match self.count.focused {
|
||||
None if self.current == 0 => state.focus(),
|
||||
Some(focused) if focused == self.current => state.unfocus(),
|
||||
|
|
@ -179,7 +199,12 @@ pub fn find_focused() -> impl Operation<Id> {
|
|||
}
|
||||
|
||||
impl Operation<Id> for FindFocused {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if state.is_focused() && id.is_some() {
|
||||
self.focused = id.cloned();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.snap_to(self.offset);
|
||||
|
|
@ -74,11 +74,11 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.scroll_to(self.offset);
|
||||
|
|
@ -109,11 +109,11 @@ pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.scroll_by(self.offset, bounds, content_bounds);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,12 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_front();
|
||||
|
|
@ -53,7 +58,12 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_end();
|
||||
|
|
@ -84,7 +94,12 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to(self.position);
|
||||
|
|
@ -113,7 +128,12 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.select_all();
|
||||
|
|
|
|||
|
|
@ -267,6 +267,18 @@ where
|
|||
|
||||
draw(renderer, defaults, layout, state.0.raw(), style, viewport);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn super::Operation,
|
||||
) {
|
||||
dbg!(&self.fragment);
|
||||
|
||||
operation.text(None, layout.bounds(), &self.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces the [`layout::Node`] of a [`Text`] widget.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ uuid = { version = "1.0", features = ["js"] }
|
|||
web-sys = { workspace = true, features = ["Window", "Storage"] }
|
||||
wasm-timer.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
iced_test.workspace = true
|
||||
|
||||
[package.metadata.deb]
|
||||
assets = [
|
||||
["target/release-opt/todos", "usr/bin/iced-todos", "755"],
|
||||
|
|
|
|||
|
|
@ -584,3 +584,36 @@ impl SavedState {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use iced::test;
|
||||
use iced::test::selector;
|
||||
|
||||
#[test]
|
||||
fn it_creates_a_new_task() {
|
||||
let (mut todos, _command) = Todos::new();
|
||||
let _command = todos.update(Message::Loaded(Err(LoadError::File)));
|
||||
|
||||
let mut interface = test::interface(todos.view());
|
||||
|
||||
let _input = interface
|
||||
.click("new-task")
|
||||
.expect("new-task input must be present");
|
||||
|
||||
interface.typewrite("Create the universe");
|
||||
interface.press_key(keyboard::key::Named::Enter);
|
||||
|
||||
for message in interface.into_messages() {
|
||||
let _command = todos.update(message);
|
||||
}
|
||||
|
||||
let mut interface = test::interface(todos.view());
|
||||
|
||||
let _ = interface
|
||||
.find(selector::text("Create the universe"))
|
||||
.expect("New task must be present");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -479,6 +479,7 @@ use iced_winit::runtime;
|
|||
|
||||
pub use iced_futures::futures;
|
||||
pub use iced_futures::stream;
|
||||
pub use iced_test as test;
|
||||
|
||||
#[cfg(feature = "highlighter")]
|
||||
pub use iced_highlighter as highlighter;
|
||||
|
|
@ -624,6 +625,7 @@ pub use error::Error;
|
|||
pub use event::Event;
|
||||
pub use executor::Executor;
|
||||
pub use font::Font;
|
||||
pub use program::Program;
|
||||
pub use renderer::Renderer;
|
||||
pub use settings::Settings;
|
||||
pub use task::Task;
|
||||
|
|
|
|||
21
test/Cargo.toml
Normal file
21
test/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "iced_test"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
categories.workspace = true
|
||||
keywords.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
iced_runtime.workspace = true
|
||||
iced_tiny_skia.workspace = true
|
||||
|
||||
iced_renderer.workspace = true
|
||||
iced_renderer.features = ["tiny-skia"]
|
||||
296
test/src/lib.rs
Normal file
296
test/src/lib.rs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
//! Test your `iced` applications in headless mode.
|
||||
#![allow(missing_docs, missing_debug_implementations)]
|
||||
pub mod selector;
|
||||
|
||||
pub use selector::Selector;
|
||||
|
||||
use iced_renderer as renderer;
|
||||
use iced_runtime as runtime;
|
||||
use iced_runtime::core;
|
||||
use iced_tiny_skia as tiny_skia;
|
||||
|
||||
use crate::core::clipboard;
|
||||
use crate::core::keyboard;
|
||||
use crate::core::mouse;
|
||||
use crate::core::widget;
|
||||
use crate::core::{Element, Event, Font, Pixels, Rectangle, Size, SmolStr};
|
||||
use crate::renderer::Renderer;
|
||||
use crate::runtime::user_interface;
|
||||
use crate::runtime::UserInterface;
|
||||
|
||||
pub fn interface<'a, Message, Theme>(
|
||||
element: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Interface<'a, Message, Theme, Renderer> {
|
||||
let mut renderer = Renderer::Secondary(tiny_skia::Renderer::new(
|
||||
Font::default(),
|
||||
Pixels(16.0),
|
||||
));
|
||||
|
||||
let raw = UserInterface::build(
|
||||
element,
|
||||
Size::new(1024.0, 1024.0),
|
||||
user_interface::Cache::default(),
|
||||
&mut renderer,
|
||||
);
|
||||
|
||||
Interface {
|
||||
raw,
|
||||
renderer,
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Interface<'a, Message, Theme, Renderer> {
|
||||
raw: UserInterface<'a, Message, Theme, Renderer>,
|
||||
renderer: Renderer,
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
pub struct Target {
|
||||
bounds: Rectangle,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Interface<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
pub fn find(
|
||||
&mut self,
|
||||
selector: impl Into<Selector>,
|
||||
) -> Result<Target, Error> {
|
||||
let selector = selector.into();
|
||||
|
||||
match &selector {
|
||||
Selector::Id(id) => {
|
||||
struct FindById<'a> {
|
||||
id: &'a widget::Id,
|
||||
target: Option<Target>,
|
||||
}
|
||||
|
||||
impl widget::Operation for FindById<'_> {
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn click(
|
||||
&mut self,
|
||||
selector: impl Into<Selector>,
|
||||
) -> Result<Target, Error> {
|
||||
let target = self.find(selector)?;
|
||||
|
||||
let _ = self.raw.update(
|
||||
&[
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
],
|
||||
mouse::Cursor::Available(target.bounds.center()),
|
||||
&mut self.renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut self.messages,
|
||||
);
|
||||
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
pub fn typewrite(&mut self, text: impl AsRef<str>) {
|
||||
let events: Vec<_> = text
|
||||
.as_ref()
|
||||
.chars()
|
||||
.map(|c| SmolStr::new_inline(&c.to_string()))
|
||||
.flat_map(|c| {
|
||||
key_press_and_release(
|
||||
keyboard::Key::Character(c.clone()),
|
||||
Some(c),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _ = self.raw.update(
|
||||
&events,
|
||||
mouse::Cursor::Unavailable,
|
||||
&mut self.renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut self.messages,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn press_key(&mut self, key: impl Into<keyboard::Key>) {
|
||||
let _ = self.raw.update(
|
||||
&key_press_and_release(key, None),
|
||||
mouse::Cursor::Unavailable,
|
||||
&mut self.renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut self.messages,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn into_messages(self) -> impl IntoIterator<Item = Message> {
|
||||
self.messages
|
||||
}
|
||||
}
|
||||
|
||||
fn key_press_and_release(
|
||||
key: impl Into<keyboard::Key>,
|
||||
text: Option<SmolStr>,
|
||||
) -> [Event; 2] {
|
||||
let key = key.into();
|
||||
|
||||
[
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: key.clone(),
|
||||
modified_key: key.clone(),
|
||||
physical_key: keyboard::key::Physical::Unidentified(
|
||||
keyboard::key::NativeCode::Unidentified,
|
||||
),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::default(),
|
||||
text,
|
||||
}),
|
||||
Event::Keyboard(keyboard::Event::KeyReleased {
|
||||
key: key.clone(),
|
||||
modified_key: key,
|
||||
physical_key: keyboard::key::Physical::Unidentified(
|
||||
keyboard::key::NativeCode::Unidentified,
|
||||
),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::default(),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
NotFound(Selector),
|
||||
}
|
||||
24
test/src/selector.rs
Normal file
24
test/src/selector.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use crate::core::text;
|
||||
use crate::core::widget;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Selector {
|
||||
Id(widget::Id),
|
||||
Text(text::Fragment<'static>),
|
||||
}
|
||||
|
||||
impl From<widget::Id> for Selector {
|
||||
fn from(id: widget::Id) -> Self {
|
||||
Self::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Selector {
|
||||
fn from(id: &'static str) -> Self {
|
||||
Self::Id(widget::Id::new(id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector {
|
||||
Selector::Text(fragment.into_fragment())
|
||||
}
|
||||
|
|
@ -445,6 +445,16 @@ where
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
operation.text(None, layout.bounds(), &self.label);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
|
||||
|
|
|
|||
|
|
@ -493,11 +493,11 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
|
|||
impl Operation<Option<Rectangle>> for VisibleBounds {
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
_id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
match self.scrollables.last() {
|
||||
Some((last_translation, last_viewport, _depth)) => {
|
||||
|
|
|
|||
|
|
@ -487,11 +487,11 @@ where
|
|||
state.translation(self.direction, bounds, content_bounds);
|
||||
|
||||
operation.scrollable(
|
||||
state,
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
|
||||
operation.container(
|
||||
|
|
|
|||
|
|
@ -971,13 +971,13 @@ where
|
|||
fn operate(
|
||||
&self,
|
||||
tree: &mut widget::Tree,
|
||||
_layout: Layout<'_>,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State<Highlighter>>();
|
||||
|
||||
operation.focusable(state, None);
|
||||
operation.focusable(None, layout.bounds(), state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -617,14 +617,23 @@ where
|
|||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
|
||||
operation.text_input(state, self.id.as_ref().map(|id| &id.0));
|
||||
operation.focusable(
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
layout.bounds(),
|
||||
state,
|
||||
);
|
||||
|
||||
operation.text_input(
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
layout.bounds(),
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn update(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue