Files
ARCADIA-Studio/crates/oa-desktop/src/app/transition_context_tests.rs
T

316 lines
12 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);
let accepted = app.document.snapshot();
// Enter through the actual navigation path: assigning Page/owner alone does
// not create the representation and live visit required by canvas events.
send(&mut app, Message::Select(root));
send(&mut app, Message::Page(Page::Behavior));
assert_eq!(app.layer, layer);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.selected, root);
assert_eq!(app.behavior.active_owner, Some(root));
if kind == Kind::Scenario {
let context = app.scenario_context().unwrap();
assert_eq!(context.document, app.canvas_document);
assert_eq!(context.scenario, root);
assert_eq!(
context.representation,
crate::behavior::scenario_layout::Representation::Derived { owner: root }
);
assert!(!context.visit.is_nil());
}
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
assert_eq!(app.source_workspace.bundle(), accepted.bundle);
assert!(!app.document.can_undo() && !app.document.can_redo());
(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()));
}