Files
ARCADIA-Studio/crates/oa-desktop/src/diagram/lifecycle_tests.rs
T

289 lines
9.2 KiB
Rust

use crate::{
app::{App, Message},
connection_edit,
connection_tool::Tool,
context_menu::Entry,
diagram::{DiagramCanvas, State},
model_menu, port_edit,
};
use iced::{
Point, Rectangle, Size, Vector, keyboard, mouse,
widget::canvas::{self, Program},
};
use oa_core::*;
fn fixture() -> (App, Id, Id) {
let mut p = Project::empty();
let a = p.add(Kind::Function, "Transmit", 100., 200.);
let b = p.add(Kind::Function, "Receive", 500., 200.);
let output = Id::new_v4();
let input = Id::new_v4();
p.elements.insert(
output,
make_port(
&p,
output,
a,
"Send",
PortContract::FunctionOutput {
outgoing_items: vec![],
},
)
.unwrap(),
);
p.elements.insert(
input,
make_port(
&p,
input,
b,
"Receive",
PortContract::FunctionInput {
incoming_items: vec![],
},
)
.unwrap(),
);
let mut app = App::with_project(p);
app.pan = Vector::new(20., 30.);
(app, output, input)
}
fn bounds() -> Rectangle {
Rectangle::new(Point::new(260., 100.), Size::new(2200., 1600.))
}
fn canvas(app: &App) -> DiagramCanvas<'_> {
DiagramCanvas {
project: app.project(),
view_index: None,
diagram: app.active_diagram.unwrap(),
selected: app.selected,
navigation_focus: None,
zoom: 1.,
pan: app.pan,
context: app.route_context().unwrap(),
selected_bend: None,
selected_attachment: None,
editable: true,
placing: false,
connection: app.connection.as_ref(),
allocation: None,
}
}
fn screen(app: &App, id: Id) -> Point {
let v = projection::project_view(app.project(), app.active_diagram.unwrap());
let p = v.ports.iter().find(|p| p.element == id).unwrap().position;
Point::new(bounds().x + app.pan.x + p.x, bounds().y + app.pan.y + p.y)
}
fn event(c: &DiagramCanvas<'_>, s: &mut State, e: mouse::Event, p: Point) -> Option<Message> {
c.update(
s,
&canvas::Event::Mouse(e),
bounds(),
mouse::Cursor::Available(p),
)
.and_then(|a| a.into_inner().0)
}
fn choose(app: &App, caption: &str) -> Message {
model_menu::popup(app)
.unwrap()
.entries
.into_iter()
.find_map(|e| match e {
Entry::Action { label, message, .. } if label == caption => message,
_ => None,
})
.unwrap_or_else(|| panic!("No action {caption}"))
}
fn send(app: &mut App, m: Message) {
let _ = app.update(m);
}
#[test]
fn middle_and_arrow_panning_preserve_source_and_history() {
for armed in [false, true] {
let (mut app, a, _) = fixture();
send(
&mut app,
Message::Connection(connection_edit::Event::Choose(Some(
Tool::FunctionalExchange,
))),
);
let context = app.route_context().unwrap();
if armed {
send(
&mut app,
Message::Connection(connection_edit::Event::Endpoint(context, a)),
);
}
let before = app.document.bundle().clone();
let pan = app.pan;
let mut state = State::default();
let start = screen(&app, a);
let end = start + Vector::new(140., 70.);
let c = canvas(&app);
let messages: Vec<_> = [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Middle),
mouse::Event::CursorMoved { position: end },
mouse::Event::ButtonReleased(mouse::Button::Middle),
]
.into_iter()
.filter_map(|e| event(&c, &mut state, e, end))
.collect();
for m in messages {
send(&mut app, m);
}
assert_eq!(app.pan, pan + Vector::new(140., 70.));
assert_eq!(app.connection.as_ref().unwrap().source, armed.then_some(a));
let e = canvas::Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::ArrowRight),
modified_key: keyboard::Key::Named(keyboard::key::Named::ArrowRight),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::ArrowRight),
location: keyboard::Location::Standard,
modifiers: keyboard::Modifiers::empty(),
text: None,
repeat: false,
});
let message = canvas(&app)
.update(&mut state, &e, bounds(), mouse::Cursor::Available(end))
.unwrap()
.into_inner()
.0
.unwrap();
send(&mut app, message);
assert_eq!(app.pan, pan + Vector::new(60., 70.));
assert_eq!(app.connection.as_ref().unwrap().context, context);
assert_eq!(app.document.bundle(), &before);
}
}
#[test]
fn menu_reset_and_draw_use_fresh_open_context() {
let (mut app, a, b) = fixture();
let c = app.route_context().unwrap();
let v = projection::project_view(app.project(), c.diagram);
let owner = v.ports.iter().find(|p| p.element == a).unwrap().owner;
send(
&mut app,
Message::PortAnchor(port_edit::Event::Commit(
c,
owner,
PortAnchor {
port: a,
side: PortAnchorSide::Top,
offset: 0.4,
},
)),
);
let before = app.document.bundle().clone();
let revision = app.document.revision();
let mut request = model_menu::Request::object(a, screen(&app, a));
request.canvas = app.route_context();
let old = request.canvas.unwrap();
send(&mut app, Message::Menu(model_menu::Event::Open(request)));
assert_ne!(app.route_context().unwrap().epoch, old.epoch);
let action = choose(&app, "Reset automatic position");
send(&mut app, action);
assert!(
app.project().diagrams[&c.diagram].nodes[&owner]
.port_anchors
.is_empty()
);
assert_eq!(app.document.revision(), revision + 1);
send(&mut app, Message::Undo);
assert_eq!(app.document.bundle(), &before);
let mut request = model_menu::Request::object(a, screen(&app, a));
request.canvas = app.route_context();
send(&mut app, Message::Menu(model_menu::Event::Open(request)));
let caption = model_menu::popup(&app)
.unwrap()
.entries
.iter()
.find_map(|e| match e {
Entry::Action { label, .. } if label.starts_with("Draw functional exchange") => {
Some(label.clone())
}
_ => None,
})
.unwrap();
let action = choose(&app, &caption);
send(&mut app, action);
assert_eq!(app.connection.as_ref().unwrap().source, Some(a));
let context = app.route_context().unwrap();
assert_eq!(app.connection.as_ref().unwrap().context, context);
send(
&mut app,
Message::Connection(connection_edit::Event::Endpoint(context, b)),
);
assert_eq!(app.project().relations.len(), 1);
let accepted = app.document.bundle().clone();
assert_eq!(
source_project::compile_bundle(&accepted, Default::default())
.unwrap()
.decoded
.project,
*app.project()
);
send(&mut app, Message::Undo);
assert_eq!(app.document.bundle(), &before);
}
#[test]
fn motion_after_release_must_not_move_clicked_port() {
let (mut app, a, _) = fixture();
let before = app.document.bundle().clone();
let start = screen(&app, a);
let end = start + Vector::new(-120., -150.);
let mut state = State::default();
let c = canvas(&app);
// Iced batches all window events until AboutToWait and passes the final
// window cursor to every event, even when the final motion follows release.
let messages: Vec<_> = [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::ButtonReleased(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
]
.into_iter()
.filter_map(|e| event(&c, &mut state, e, end))
.collect();
for m in messages {
send(&mut app, m);
}
assert!(
app.document.bundle() == &before,
"Moving the cursor after release must not author a port anchor"
);
}
#[test]
fn motion_after_source_click_release_must_not_create_connection() {
let (mut app, a, b) = fixture();
send(
&mut app,
Message::Connection(connection_edit::Event::Choose(Some(
Tool::FunctionalExchange,
))),
);
let start = screen(&app, a);
let end = screen(&app, b);
let mut state = State::default();
let c = canvas(&app);
let messages: Vec<_> = [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::ButtonReleased(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
]
.into_iter()
.filter_map(|e| event(&c, &mut state, e, end))
.collect();
for m in messages {
send(&mut app, m);
}
assert!(
app.project().relations.is_empty(),
"Hover after releasing the source is not a connection commit"
);
}