367 lines
13 KiB
Rust
367 lines
13 KiB
Rust
// Test-only proposal pinned to peer 6520a89dc042dd433454f6cc63cb23b0e6df7911.
|
|
// Independent App test module; real popup messages retain menu tokens.
|
|
use super::*;
|
|
use crate::model_menu::{Event as MenuEvent, Request};
|
|
|
|
struct BeginFixture {
|
|
app: App,
|
|
diagram: Id,
|
|
surface_layer: Layer,
|
|
source: Id,
|
|
target: Id,
|
|
wrong_layer: Id,
|
|
hidden: Id,
|
|
tool: crate::connection_tool::Tool,
|
|
}
|
|
|
|
fn begin_fixture(source_layer: Layer, allocation: bool, mixed: bool) -> BeginFixture {
|
|
use crate::connection_tool::Tool;
|
|
let mut p = Project::empty();
|
|
let (work, performer, exchange) = match source_layer {
|
|
Layer::Oa => (Kind::Activity, Kind::Entity, Tool::Interaction),
|
|
Layer::Sa => (Kind::Function, Kind::System, Tool::FunctionalExchange),
|
|
};
|
|
let opposite = if source_layer == Layer::Oa {
|
|
Layer::Sa
|
|
} else {
|
|
Layer::Oa
|
|
};
|
|
let surface_layer = if mixed { opposite } else { source_layer };
|
|
let source = p
|
|
.add_element(
|
|
if allocation { performer } else { work },
|
|
source_layer,
|
|
"Source",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let target = p.add_element(work, source_layer, "Target", None).unwrap();
|
|
let hidden = p
|
|
.add_element(work, source_layer, "Hidden target", None)
|
|
.unwrap();
|
|
let other_work = if opposite == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
let wrong_layer = p
|
|
.add_element(other_work, opposite, "Wrong layer target", None)
|
|
.unwrap();
|
|
p.ensure_diagram(source_layer);
|
|
let diagram = p.ensure_diagram(surface_layer);
|
|
p.diagrams.get_mut(&diagram).unwrap().projection = if mixed {
|
|
ProjectionKind::Traceability
|
|
} else {
|
|
ProjectionKind::Capabilities
|
|
};
|
|
p.place(diagram, source, Position { x: 0., y: 0. });
|
|
p.place(diagram, target, Position { x: 400., y: 0. });
|
|
if mixed {
|
|
p.place(diagram, wrong_layer, Position { x: 800., y: 0. });
|
|
}
|
|
let mut app = App::with_project(p);
|
|
app.page = Page::Diagram;
|
|
app.layer = surface_layer;
|
|
app.active_diagram = Some(diagram);
|
|
app.selected = diagram;
|
|
app.refresh_draft();
|
|
app.reconcile_multiselect();
|
|
assert!(!app.document.can_undo());
|
|
BeginFixture {
|
|
app,
|
|
diagram,
|
|
surface_layer,
|
|
source,
|
|
target,
|
|
wrong_layer,
|
|
hidden,
|
|
tool: if allocation {
|
|
Tool::Allocation
|
|
} else {
|
|
exchange
|
|
},
|
|
}
|
|
}
|
|
|
|
fn begin_surface(f: &BeginFixture) {
|
|
assert_eq!(f.app.page, Page::Diagram);
|
|
assert_eq!(f.app.layer, f.surface_layer);
|
|
assert_eq!(f.app.active_diagram, Some(f.diagram));
|
|
}
|
|
|
|
fn begin_popup_action(f: &mut BeginFixture) -> Option<Message> {
|
|
begin_popup_action_from(f, true)
|
|
}
|
|
|
|
fn begin_popup_action_from(f: &mut BeginFixture, canvas: bool) -> Option<Message> {
|
|
let mut request = Request::object(f.source, iced::Point::ORIGIN);
|
|
if canvas {
|
|
request.canvas = f.app.route_context();
|
|
request.placement = f.app.project().diagrams[&f.diagram]
|
|
.nodes
|
|
.values()
|
|
.find(|node| node.element == f.source)
|
|
.map(|node| node.id);
|
|
}
|
|
let _ = f.app.update(Message::Menu(MenuEvent::Open(request)));
|
|
let popup = crate::model_menu::popup(&f.app).expect("real model popup");
|
|
let expected = format!("Draw {}", f.tool.label().to_lowercase());
|
|
for entry in popup.entries {
|
|
if let crate::context_menu::Entry::Action { label, message, .. } = entry
|
|
&& label == expected
|
|
{
|
|
if let Some(Message::Menu(MenuEvent::Invoke(token, _))) = &message {
|
|
assert_eq!(*token, popup.token);
|
|
} else {
|
|
assert!(message.is_none(), "Draw must use the menu token envelope");
|
|
}
|
|
return message;
|
|
}
|
|
}
|
|
None // Omission is also a valid unavailable-action policy.
|
|
}
|
|
|
|
fn begin_from_popup(f: &mut BeginFixture) -> Message {
|
|
begin_from_popup_origin(f, true)
|
|
}
|
|
|
|
fn begin_from_popup_origin(f: &mut BeginFixture, canvas: bool) -> Message {
|
|
let before = f.app.document.snapshot();
|
|
let revision = f.app.document.revision();
|
|
let action =
|
|
begin_popup_action_from(f, canvas).expect("visible semantic source offers enabled Draw");
|
|
let _ = f.app.update(action.clone());
|
|
begin_surface(f);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert_eq!(f.app.document.revision(), revision);
|
|
assert!(!f.app.document.can_undo());
|
|
let session = f
|
|
.app
|
|
.connection
|
|
.as_ref()
|
|
.expect("menu Draw must arm its tool");
|
|
assert_eq!(session.tool, f.tool);
|
|
assert_eq!(session.source, Some(f.source));
|
|
assert_eq!(session.context, f.app.route_context().unwrap());
|
|
assert!(session.candidates.contains(&f.target));
|
|
assert!(!session.candidates.contains(&f.wrong_layer));
|
|
assert!(!session.candidates.contains(&f.hidden));
|
|
action
|
|
}
|
|
|
|
fn begin_complete_and_check_history(f: &mut BeginFixture) {
|
|
let before = f.app.document.snapshot();
|
|
let revision = f.app.document.revision();
|
|
let context = f.app.route_context().unwrap();
|
|
let _ = f.app.update(Message::Connection(
|
|
crate::connection_edit::Event::Endpoint(context, f.target),
|
|
));
|
|
begin_surface(f);
|
|
assert!(f.app.connection.is_none());
|
|
assert_eq!(f.app.document.revision(), revision + 1);
|
|
let relations: Vec<_> = f
|
|
.app
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.filter(|r| !before.decoded.project.relations.contains_key(&r.id))
|
|
.collect();
|
|
assert_eq!(relations.len(), 1);
|
|
let expected = match f.tool {
|
|
crate::connection_tool::Tool::Interaction => RelationKind::Interaction,
|
|
crate::connection_tool::Tool::FunctionalExchange => RelationKind::FunctionalExchange,
|
|
crate::connection_tool::Tool::Allocation => RelationKind::Allocation,
|
|
_ => unreachable!(),
|
|
};
|
|
assert_eq!(relations[0].kind(), expected);
|
|
assert_eq!(
|
|
(relations[0].source(), relations[0].target()),
|
|
(f.source, f.target)
|
|
);
|
|
// Random identities are accepted only for the precise command delta.
|
|
// Everything else, including traces, metadata and view options, stays exact.
|
|
let mut expected_project = before.decoded.project.clone();
|
|
let created = relations[0];
|
|
assert!(
|
|
expected_project
|
|
.relations
|
|
.insert(created.id, created.clone())
|
|
.is_none()
|
|
);
|
|
if let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
items,
|
|
..
|
|
} = &created.body
|
|
{
|
|
assert_eq!(f.tool, crate::connection_tool::Tool::FunctionalExchange);
|
|
assert!(items.is_empty());
|
|
assert_ne!(source_port, target_port);
|
|
let output = &f.app.project().elements[source_port];
|
|
let input = &f.app.project().elements[target_port];
|
|
assert_eq!(
|
|
(output.kind(), output.layer(), output.parent),
|
|
(Kind::Port, Layer::Sa, Some(f.source))
|
|
);
|
|
assert_eq!(
|
|
(input.kind(), input.layer(), input.parent),
|
|
(Kind::Port, Layer::Sa, Some(f.target))
|
|
);
|
|
assert!(
|
|
matches!(port_contract(output), Some(PortContract::FunctionOutput { outgoing_items }) if outgoing_items.is_empty())
|
|
);
|
|
assert!(
|
|
matches!(port_contract(input), Some(PortContract::FunctionInput { incoming_items }) if incoming_items.is_empty())
|
|
);
|
|
assert!(!before.decoded.project.elements.contains_key(source_port));
|
|
assert!(!before.decoded.project.elements.contains_key(target_port));
|
|
assert_eq!(output.id, *source_port);
|
|
assert_eq!(input.id, *target_port);
|
|
expected_project
|
|
.elements
|
|
.insert(*source_port, output.clone());
|
|
expected_project
|
|
.elements
|
|
.insert(*target_port, input.clone());
|
|
assert_eq!(
|
|
f.app.project().elements.len(),
|
|
before.decoded.project.elements.len() + 2
|
|
);
|
|
} else {
|
|
assert_ne!(f.tool, crate::connection_tool::Tool::FunctionalExchange);
|
|
assert_eq!(f.app.project().elements, before.decoded.project.elements);
|
|
}
|
|
assert_eq!(f.app.project(), &expected_project);
|
|
assert_eq!(f.app.project().diagrams, before.decoded.project.diagrams);
|
|
for (id, element) in &before.decoded.project.elements {
|
|
assert_eq!(&f.app.project().elements[id], element);
|
|
}
|
|
begin_assert_source_pair(&f.app);
|
|
let after = f.app.document.snapshot();
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert!(
|
|
!f.app.document.can_undo(),
|
|
"creation is a single history step"
|
|
);
|
|
let _ = f.app.update(Message::Redo);
|
|
assert_eq!(f.app.project(), &after.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &after.bundle);
|
|
begin_assert_source_pair(&f.app);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_begin_exchange_uses_actual_menu_in_both_mixed_directions() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = begin_fixture(layer, false, true);
|
|
begin_from_popup(&mut f);
|
|
begin_complete_and_check_history(&mut f);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_begin_cancel_restart_and_stale_menu_preserve_session_ownership() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = begin_fixture(layer, false, true);
|
|
let original = f.app.document.snapshot();
|
|
let stale_menu = begin_from_popup(&mut f);
|
|
let first = f.app.route_context().unwrap();
|
|
let _ = f
|
|
.app
|
|
.update(Message::Connection(crate::connection_edit::Event::Restart(
|
|
first,
|
|
)));
|
|
begin_surface(&f);
|
|
let restarted = f
|
|
.app
|
|
.connection
|
|
.as_ref()
|
|
.expect("restart retains contextual tool");
|
|
assert_eq!(restarted.tool, f.tool);
|
|
assert_eq!(restarted.source, None);
|
|
assert!(restarted.candidates.contains(&f.source));
|
|
let current = restarted.context;
|
|
assert_ne!(current, first);
|
|
let _ = f.app.update(stale_menu);
|
|
let _ = f.app.update(Message::Connection(
|
|
crate::connection_edit::Event::Endpoint(first, f.target),
|
|
));
|
|
assert_eq!(f.app.connection.as_ref().unwrap().context, current);
|
|
assert_eq!(f.app.connection.as_ref().unwrap().source, None);
|
|
let _ = f.app.update(Message::Connection(
|
|
crate::connection_edit::Event::Endpoint(current, f.source),
|
|
));
|
|
assert_eq!(f.app.connection.as_ref().unwrap().source, Some(f.source));
|
|
let _ = f.app.update(Message::Escape);
|
|
let _ = f.app.update(Message::Connection(
|
|
crate::connection_edit::Event::Endpoint(current, f.target),
|
|
));
|
|
assert!(f.app.connection.is_none());
|
|
begin_surface(&f);
|
|
assert_eq!(f.app.project(), &original.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &original.bundle);
|
|
assert!(!f.app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_begin_allocation_uses_actual_menu_in_both_mixed_directions() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = begin_fixture(layer, true, true);
|
|
begin_from_popup(&mut f);
|
|
begin_complete_and_check_history(&mut f);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_begin_allocation_is_not_enabled_when_projection_hides_work() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = begin_fixture(layer, true, false);
|
|
let view = project_view(f.app.project(), f.diagram);
|
|
assert!(view.nodes.iter().any(|n| n.element == f.source));
|
|
assert!(!view.nodes.iter().any(|n| n.element == f.target));
|
|
assert!(!f.tool.available_in(layer, ProjectionKind::Capabilities));
|
|
let before = f.app.document.snapshot();
|
|
assert!(
|
|
begin_popup_action(&mut f).is_none(),
|
|
"Draw allocation must be disabled or omitted when work is not projected"
|
|
);
|
|
let _ = f.app.update(Message::Menu(MenuEvent::Close));
|
|
// Defensive handler refusal complements actual-menu enabledness above.
|
|
let _ = f
|
|
.app
|
|
.update(Message::Connection(crate::connection_edit::Event::Begin(
|
|
f.tool, f.source,
|
|
)));
|
|
assert!(f.app.connection.is_none());
|
|
begin_surface(&f);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert!(!f.app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
fn begin_assert_source_pair(app: &App) {
|
|
let compiled = oa_core::source_project::compile_bundle(
|
|
app.document.bundle(),
|
|
oa_core::source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&compiled.decoded.project, app.project());
|
|
assert_eq!(&compiled.bundle, app.document.bundle());
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_begin_explorer_draw_retains_visible_mixed_diagram_in_both_directions() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = begin_fixture(layer, false, true);
|
|
// A real Explorer Request::object has no canvas/placement envelope,
|
|
// but its generated Draw action still targets the visible current view.
|
|
begin_from_popup_origin(&mut f, false);
|
|
begin_complete_and_check_history(&mut f);
|
|
}
|
|
}
|