Preserve nested architecture during authoring and distinguish allocation summaries

This commit is contained in:
2026-09-05 17:58:46 +03:00
parent c20a2073d7
commit c7101b970a
13 changed files with 2719 additions and 1653 deletions
+296
View File
@@ -936,3 +936,299 @@ fn behavior_and_data_authoring_do_not_place_nodes_in_unrelated_architecture_view
}
}
}
#[test]
fn revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner() {
let mut p = Project::demo();
let diagram = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Allocation)
.unwrap()
.id;
let view = oa_core::projection::project_view(&p, diagram);
let function = view
.nodes
.iter()
.find(|n| p.elements[&n.element].kind() == Kind::Function && n.visual_parent.is_some())
.unwrap();
let ancestor = function.visual_parent.unwrap();
let port = p
.children(function.element)
.find(|e| e.kind() == Kind::Port)
.unwrap()
.id;
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&ancestor)
.unwrap()
.collapsed = true;
assert!(crate::navigation::diagrams_containing(&p, port).contains(&diagram));
let mut app = App::with_project(p);
send(&mut app, Message::RevealInDiagram(diagram, port));
assert_eq!(app.selected, port);
assert_eq!(app.active_diagram, Some(diagram));
assert!(!app.project().diagrams[&diagram].nodes[&ancestor].collapsed);
assert!(
oa_core::projection::project_view(app.project(), diagram)
.ports
.iter()
.any(|p| p.element == port)
);
}
#[test]
fn creation_from_capabilities_routes_work_to_architecture_and_folded_allocation_inserts_below_existing_work()
{
let p = Project::demo();
let system = p
.elements
.values()
.find(|e| e.kind() == Kind::System)
.unwrap()
.id;
let capabilities = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Capabilities)
.unwrap()
.id;
let mut app = App::with_project(p);
send(&mut app, Message::Layer(Layer::Sa));
send(
&mut app,
Message::Diagram(crate::app::Choice {
id: capabilities,
label: String::new(),
}),
);
send(&mut app, Message::CreateKind(Kind::Function));
send(&mut app, Message::Add(false));
let diagram = app.active_diagram.unwrap();
assert_eq!(
app.project().diagrams[&diagram].projection,
ProjectionKind::Allocation
);
assert!(
app.project().diagrams[&diagram]
.nodes
.values()
.any(|n| n.element == app.selected)
);
send(&mut app, Message::Select(system));
let owner = app.selected_node.unwrap();
send(&mut app, Message::ToggleCollapse);
assert!(app.project().diagrams[&diagram].nodes[&owner].collapsed);
assert_eq!(
app.selected, system,
"Folding must retain semantic selection"
);
let before = app.project().clone();
send(&mut app, Message::Add(true));
let created = app.selected;
let view = oa_core::projection::project_view(app.project(), diagram);
let child = view
.nodes
.iter()
.find(|n| n.element == created)
.unwrap_or_else(|| panic!("Created work is not visible: {}", app.status));
assert_eq!(child.visual_parent, Some(owner));
assert!(!app.project().diagrams[&diagram].nodes[&owner].collapsed);
assert!(
view.nodes
.iter()
.filter(|n| n.visual_parent == Some(owner) && n.id != child.id)
.all(|n| n.position.y + n.size.height <= child.position.y)
);
let parent = view.nodes.iter().find(|n| n.id == owner).unwrap();
assert!(
view.nodes
.iter()
.filter(|n| n.visual_parent.is_none() && n.id != owner)
.all(|n| n.position.x + n.size.width <= parent.position.x
|| parent.position.x + parent.size.width <= n.position.x
|| n.position.y + n.size.height <= parent.position.y
|| parent.position.y + parent.size.height <= n.position.y)
);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
}
#[test]
fn nested_allocation_insertion_reflows_sibling_subtrees_and_preserves_fold_state() {
use oa_core::projection::{project_diagram, project_view};
for folded in [false, true] {
let mut p = Project::empty();
let root = p
.add_element(Kind::Entity, Layer::Oa, "Root", None)
.unwrap();
let a = p
.add_element(Kind::Entity, Layer::Oa, "A", Some(root))
.unwrap();
let b = p
.add_element(Kind::Entity, Layer::Oa, "B", Some(root))
.unwrap();
let other = p
.add_element(Kind::Entity, Layer::Oa, "Other root", None)
.unwrap();
let a_work = p
.add_element(Kind::Activity, Layer::Oa, "A work", None)
.unwrap();
let b_work = p
.add_element(Kind::Activity, Layer::Oa, "B work", None)
.unwrap();
let b_port = p
.add_element(Kind::Port, Layer::Oa, "B boundary", Some(b))
.unwrap();
p.relate(RelationKind::Allocation, "A performs", a, a_work);
p.relate(RelationKind::Allocation, "B performs", b, b_work);
let diagram = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
for id in [root, a, b, other, a_work, b_work] {
p.place(diagram, id, Position { x: 60., y: 60. });
}
let mut p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
let view = project_view(&p, diagram);
let root_node = view.nodes.iter().find(|n| n.element == root).unwrap();
let a_node = view.nodes.iter().find(|n| n.element == a).unwrap();
let b_node = view.nodes.iter().find(|n| n.element == b).unwrap();
// Layout's UUID order is intentionally irrelevant to this scenario:
// explicitly put A before B, as a user can arrange these participants.
p = execute_batch(
&p,
&[
Command::MoveNode {
diagram,
node: a_node.id,
position: Position {
x: root_node.position.x + 24.,
y: root_node.position.y + 64.,
},
with_descendants: true,
},
Command::MoveNode {
diagram,
node: b_node.id,
position: Position {
x: root_node.position.x + 24.,
y: root_node.position.y + 64. + a_node.size.height + 16.,
},
with_descendants: true,
},
],
)
.unwrap();
let view = project_view(&p, diagram);
let root_node = view.nodes.iter().find(|n| n.element == root).unwrap();
let other_node = view.nodes.iter().find(|n| n.element == other).unwrap().id;
let b_node = view.nodes.iter().find(|n| n.element == b).unwrap().id;
let b_work_node = view.nodes.iter().find(|n| n.element == b_work).unwrap().id;
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&other_node)
.unwrap()
.position = Position {
x: root_node.position.x,
y: root_node.position.y + root_node.size.height + 24.,
};
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&b_node)
.unwrap()
.collapsed = folded;
// Deliberately overlapping unrelated roots must not be rearranged.
let mut unrelated = Vec::new();
for name in ["Unrelated one", "Unrelated two"] {
let id = p.add_element(Kind::Entity, Layer::Oa, name, None).unwrap();
unrelated.push(p.place(diagram, id, Position { x: 2000., y: 1000. }));
}
let before = p.clone();
let mut expanded = p.diagrams[&diagram].clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let before_view = project_diagram(&p, &expanded);
let before_port = before_view
.ports
.iter()
.find(|p| p.element == b_port)
.unwrap()
.position;
let mut app = App::with_project(p);
send(&mut app, Message::Select(a));
send(&mut app, Message::CreateKind(Kind::Activity));
send(&mut app, Message::Add(true));
let created = app.selected;
assert_eq!(
app.project().elements[&created].kind(),
Kind::Activity,
"{}",
app.status
);
let after = &app.project().diagrams[&diagram];
assert_eq!(after.nodes[&b_node].collapsed, folded);
let dy =
after.nodes[&b_node].position.y - before.diagrams[&diagram].nodes[&b_node].position.y;
assert!(dy > 0., "B must move below the enlarged A");
assert_eq!(
after.nodes[&b_work_node].position.y,
before.diagrams[&diagram].nodes[&b_work_node].position.y + dy
);
assert!(
after.nodes[&other_node].position.y
> before.diagrams[&diagram].nodes[&other_node].position.y
);
for id in unrelated {
assert_eq!(after.nodes[&id], before.diagrams[&diagram].nodes[&id]);
}
let mut expanded = after.clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let view = project_diagram(app.project(), &expanded);
let after_port = view
.ports
.iter()
.find(|p| p.element == b_port)
.unwrap()
.position;
assert_eq!(
after_port,
Position {
x: before_port.x,
y: before_port.y + dy
}
);
for (left, right) in [(a, b), (root, other), (a_work, created)] {
let left = view.nodes.iter().find(|n| n.element == left).unwrap();
let right = view.nodes.iter().find(|n| n.element == right).unwrap();
assert!(left.position.y + left.size.height <= right.position.y);
}
assert!(crate::diagram::insertion_adjustments(app.project(), diagram, created).is_empty());
send(&mut app, Message::Undo);
assert_eq!(
app.project(),
&before,
"Creation and all reflow must undo together"
);
if folded {
send(&mut app, Message::Select(root));
send(&mut app, Message::CreateKind(Kind::Entity));
send(&mut app, Message::Add(true));
let mut expanded = app.project().diagrams[&diagram].clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let view = project_diagram(app.project(), &expanded);
let b = view.nodes.iter().find(|n| n.id == b_node).unwrap();
let created = view
.nodes
.iter()
.find(|n| n.element == app.selected)
.unwrap();
assert!(b.position.y + b.size.height <= created.position.y);
assert!(app.project().diagrams[&diagram].nodes[&b_node].collapsed);
}
}
}