299 lines
11 KiB
Rust
299 lines
11 KiB
Rust
//! Transition navigation must not add presentation edits to a behavior review.
|
|
use super::*;
|
|
use crate::workflow::{Event, Workflow};
|
|
use oa_core::transitions::*;
|
|
|
|
fn send(app: &mut App, message: Message) {
|
|
let _ = app.update(message);
|
|
}
|
|
|
|
fn element(
|
|
p: &mut Project,
|
|
number: u128,
|
|
kind: Kind,
|
|
layer: Layer,
|
|
name: &str,
|
|
parent: Option<Id>,
|
|
) -> Id {
|
|
let mut e = p.make_element(kind, layer, name, parent).unwrap();
|
|
e.id = Id::from_u128(number);
|
|
let id = e.id;
|
|
p.elements.insert(id, e);
|
|
id
|
|
}
|
|
|
|
fn fixture(layer: Layer, kind: Kind) -> (App, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let other = if layer == Layer::Oa {
|
|
Layer::Sa
|
|
} else {
|
|
Layer::Oa
|
|
};
|
|
let work_kind = |l| {
|
|
if l == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
}
|
|
};
|
|
let work = element(&mut p, 1, work_kind(layer), layer, "Assess pressure", None);
|
|
let reused = element(&mut p, 10, work_kind(other), other, "Assess pressure", None);
|
|
let root = element(&mut p, 100, kind, layer, "Restore service", None);
|
|
if kind == Kind::Scenario {
|
|
let line = element(&mut p, 2, Kind::Lifeline, layer, "Assessment", Some(root));
|
|
*p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(work),
|
|
};
|
|
}
|
|
let view = p.ensure_diagram(other);
|
|
p.diagrams.get_mut(&view).unwrap().name = "Unrelated architecture".into();
|
|
p.place(view, reused, Position { x: 873., y: 421. });
|
|
let mut app = App::with_project(p);
|
|
app.layer = layer;
|
|
app.page = Page::Behavior;
|
|
app.selected = root;
|
|
app.behavior.active_owner = Some(root);
|
|
app.refresh_draft();
|
|
(app, root, work, reused)
|
|
}
|
|
|
|
fn review(
|
|
app: &mut App,
|
|
selected: Id,
|
|
mapping: Option<(Id, Id)>,
|
|
destination: Option<Id>,
|
|
) -> (Id, Project) {
|
|
app.selected = selected;
|
|
app.refresh_draft();
|
|
send(app, Message::StartTransition);
|
|
let Some(Workflow::Transition(t)) = &mut app.workflow else {
|
|
panic!("{}", app.status)
|
|
};
|
|
t.request.scope = DependencyScope::Subtree;
|
|
t.request.destination_diagram = destination;
|
|
if let Some((source, target)) = mapping {
|
|
t.request.mappings.push(TransitionMapping {
|
|
source,
|
|
target: Some(target),
|
|
target_kind: None,
|
|
});
|
|
}
|
|
t.plan = plan_transition(app.document.project(), &t.request).unwrap();
|
|
assert!(t.plan.conflicts.is_empty(), "{:?}", t.plan.conflicts);
|
|
let target = t
|
|
.plan
|
|
.changes
|
|
.iter()
|
|
.find(|c| c.source == selected)
|
|
.unwrap()
|
|
.target;
|
|
let expected = apply_transition(app.document.project(), &t.plan).unwrap();
|
|
(target, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_transition_focuses_requested_counterpart_and_preserves_unrelated_views_in_both_directions()
|
|
{
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let (mut app, root, work, reused) = fixture(layer, Kind::Scenario);
|
|
let stale_selection = crate::behavior::scenario::CanvasEvent::Select(
|
|
app.scenario_context().unwrap(),
|
|
Id::from_u128(2),
|
|
);
|
|
let before = app.document.snapshot();
|
|
let (target, expected) = review(&mut app, root, Some((work, reused)), None);
|
|
let Some(Workflow::Transition(t)) = &app.workflow else {
|
|
unreachable!()
|
|
};
|
|
assert_ne!(
|
|
t.plan.changes.first().unwrap().source,
|
|
root,
|
|
"Fixture must expose a dependency before the selected root"
|
|
);
|
|
app.selected_node = Some(Id::from_u128(999));
|
|
send(&mut app, Message::Workflow(Event::Apply));
|
|
assert_eq!(
|
|
app.project(),
|
|
&expected,
|
|
"Desktop must preserve the core candidate exactly"
|
|
);
|
|
assert_eq!(app.project().diagrams, before.decoded.project.diagrams);
|
|
assert_eq!(app.page, Page::Behavior);
|
|
assert_eq!(app.selected, target);
|
|
assert_eq!(app.behavior.active_owner, Some(target));
|
|
assert_eq!(app.scenario_context().unwrap().scenario, target);
|
|
assert_eq!(app.selected_node, None);
|
|
assert!(app.workflow.is_none());
|
|
assert_eq!(app.document.revision(), 1);
|
|
let accepted = app.document.snapshot();
|
|
send(&mut app, Message::ScenarioCanvas(stale_selection.clone()));
|
|
assert_eq!(app.selected, target);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
assert_eq!(
|
|
source_project::compile_bundle(&accepted.bundle, LIMITS)
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
expected
|
|
);
|
|
send(&mut app, Message::Undo);
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert!(!app.document.can_undo());
|
|
let undone_selection = app.selected;
|
|
send(&mut app, Message::ScenarioCanvas(stale_selection));
|
|
assert_eq!(app.selected, undone_selection);
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
send(&mut app, Message::Redo);
|
|
assert_eq!(app.project(), &accepted.decoded.project);
|
|
assert_eq!(app.document.bundle(), &accepted.bundle);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_behavior_internal_and_process_roots_keep_their_workspace_without_creating_a_view() {
|
|
for (layer, kind) in [
|
|
(Layer::Oa, Kind::Scenario),
|
|
(Layer::Sa, Kind::Scenario),
|
|
(Layer::Oa, Kind::Process),
|
|
(Layer::Sa, Kind::FunctionalChain),
|
|
] {
|
|
let (mut app, root, work, reused) = fixture(layer, kind);
|
|
let mut p = app.project().clone();
|
|
p.diagrams.clear();
|
|
app = App::with_project(p);
|
|
app.page = Page::Behavior;
|
|
let selected = if kind == Kind::Scenario {
|
|
Id::from_u128(2)
|
|
} else {
|
|
root
|
|
};
|
|
let (target, expected) = review(&mut app, selected, Some((work, reused)), None);
|
|
let owner = crate::behavior::selected_behavior(&expected, target).unwrap();
|
|
send(&mut app, Message::Workflow(Event::Apply));
|
|
assert_eq!(app.project(), &expected);
|
|
assert!(app.project().diagrams.is_empty());
|
|
assert_eq!(app.selected, target);
|
|
assert_eq!(app.behavior.active_owner, Some(owner));
|
|
assert_eq!(app.page, Page::Behavior);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_destination_is_honored_and_ordinary_work_transition_still_places_in_architecture() {
|
|
let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario);
|
|
let destination = *app.project().diagrams.keys().next().unwrap();
|
|
let (target, expected) = review(&mut app, root, Some((work, reused)), Some(destination));
|
|
assert!(
|
|
expected.diagrams[&destination]
|
|
.nodes
|
|
.values()
|
|
.any(|n| n.element == target)
|
|
);
|
|
send(&mut app, Message::Workflow(Event::Apply));
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.page, Page::Behavior);
|
|
let (mut app, _, work, _) = fixture(Layer::Oa, Kind::Scenario);
|
|
let (target, _) = review(&mut app, work, None, None);
|
|
send(&mut app, Message::Workflow(Event::Apply));
|
|
assert_eq!(app.page, Page::Diagram);
|
|
assert_eq!(app.selected, target);
|
|
assert!(
|
|
app.project().diagrams[&app.active_diagram.unwrap()]
|
|
.nodes
|
|
.values()
|
|
.any(|n| n.element == target)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn machine_and_typed_action_transitions_resolve_the_owner_and_reusing_that_context_retains_camera()
|
|
{
|
|
for selected_action in [false, true] {
|
|
let mut p = Project::empty();
|
|
let owner = element(&mut p, 1, Kind::Entity, Layer::Oa, "Control room", None);
|
|
let counterpart = element(&mut p, 10, Kind::System, Layer::Sa, "Control room", None);
|
|
let machine = element(
|
|
&mut p,
|
|
100,
|
|
Kind::StateMachine,
|
|
Layer::Oa,
|
|
"Pressure modes",
|
|
Some(owner),
|
|
);
|
|
let region = element(&mut p, 101, Kind::Region, Layer::Oa, "Main", Some(machine));
|
|
let state = element(&mut p, 102, Kind::State, Layer::Oa, "Waiting", Some(region));
|
|
let work = element(&mut p, 2, Kind::Activity, Layer::Oa, "Assess alarm", None);
|
|
let action = Id::from_u128(103);
|
|
p.relations.insert(
|
|
action,
|
|
Relation {
|
|
id: action,
|
|
alias: "assess_entry".into(),
|
|
name: "Assess on entry".into(),
|
|
description: String::new(),
|
|
body: RelationBody::StateAction {
|
|
source: state,
|
|
target: work,
|
|
phase: StateActionKind::Entry,
|
|
},
|
|
},
|
|
);
|
|
let mut app = App::with_project(p);
|
|
app.page = Page::Behavior;
|
|
app.behavior.active_owner = Some(machine);
|
|
let selected = if selected_action { action } else { machine };
|
|
let (target, expected) = review(&mut app, selected, Some((owner, counterpart)), None);
|
|
let target_machine = crate::behavior::selected_behavior(&expected, target).unwrap();
|
|
send(&mut app, Message::Workflow(Event::Apply));
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.page, Page::Behavior);
|
|
assert_eq!(app.selected, target);
|
|
assert_eq!(app.behavior.active_owner, Some(target_machine));
|
|
assert!(
|
|
app.project().diagrams.is_empty(),
|
|
"The state workspace must offer explicit view creation"
|
|
);
|
|
app.behavior.diagram.zoom = 1.17;
|
|
app.behavior.diagram.pan = iced::Vector::new(-73., 146.);
|
|
let epoch = app.canvas_epoch;
|
|
let (same_target, _) = review(&mut app, selected, Some((owner, counterpart)), None);
|
|
assert_eq!(same_target, target);
|
|
send(&mut app, Message::Workflow(Event::Apply));
|
|
assert_eq!(app.behavior.diagram.zoom, 1.17);
|
|
assert_eq!(app.behavior.diagram.pan, iced::Vector::new(-73., 146.));
|
|
assert_eq!(app.canvas_epoch, epoch);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejected_source_writeback_keeps_review_and_navigation_and_invalid_draft_still_blocks_start() {
|
|
let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario);
|
|
let bundle = app.document.bundle().clone();
|
|
let mut limits = oa_core::source_project::ProjectLimits::default();
|
|
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::<usize>() + 1;
|
|
app.document = Document::from_bundle(bundle.clone(), limits).unwrap();
|
|
let (_, _) = review(&mut app, root, Some((work, reused)), None);
|
|
let before = app.document.snapshot();
|
|
send(&mut app, Message::Workflow(Event::Apply));
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
assert!(app.workflow.is_some());
|
|
assert_eq!(app.page, Page::Behavior);
|
|
assert_eq!(app.selected, root);
|
|
assert_eq!(app.behavior.active_owner, Some(root));
|
|
assert!(!app.document.can_undo());
|
|
app.workflow = None;
|
|
send(
|
|
&mut app,
|
|
Message::Field("/alias".into(), "not an alias!".into()),
|
|
);
|
|
let draft = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
|
send(&mut app, Message::StartTransition);
|
|
assert!(app.workflow.is_none());
|
|
assert_eq!(
|
|
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
|
draft
|
|
);
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
}
|