Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -16,6 +16,7 @@ impl App {
|
||||
home::Event::New => self.handle(Message::Request(Pending::New)),
|
||||
home::Event::Open => self.handle(Message::Request(Pending::Open)),
|
||||
home::Event::Example => self.handle(Message::Request(Pending::Demo)),
|
||||
home::Event::Walkthrough => self.handle(Message::Request(Pending::Walkthrough)),
|
||||
home::Event::Guide => self.update(Message::Page(Page::Guide)),
|
||||
home::Event::Resume => self.update(Message::Page(self.last_workspace_page)),
|
||||
home::Event::OpenRecent(path) => {
|
||||
@@ -151,6 +152,7 @@ impl App {
|
||||
self.reset(restored.document.project().clone(), None);
|
||||
self.hosted.leave_document();
|
||||
self.document = restored.document;
|
||||
self.migration = self.document.compiled().decoded.migration.clone();
|
||||
self.selected = if self.project().layer_of(restored.selected).is_some()
|
||||
|| restored.selected == self.project().id
|
||||
{
|
||||
@@ -184,7 +186,11 @@ impl App {
|
||||
} else {
|
||||
Page::Diagram
|
||||
};
|
||||
self.status="Recovered an unsaved local copy. Choose Save as to save it; the original file and hosted model are unchanged.".into();
|
||||
self.status = if self.migration.is_some() {
|
||||
"Recovered source 5 as source 6 in an unsaved local copy. Draft text is preserved; workflow previews were rebuilt and semantic impact approvals need review. Choose Save as to save it."
|
||||
} else {
|
||||
"Recovered an unsaved local copy. Choose Save as to save it; the original file and hosted model are unchanged."
|
||||
}.into();
|
||||
self.request_lint()
|
||||
}
|
||||
}
|
||||
@@ -279,6 +285,28 @@ mod tests {
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walkthrough_request_keeps_unfinished_source_behind_the_normal_pending_guard() {
|
||||
let mut app = App::with_project(Project::demo());
|
||||
let accepted = app.document.snapshot();
|
||||
let raw = format!(
|
||||
"{}\nunfinished walkthrough draft",
|
||||
app.source_workspace.text()
|
||||
);
|
||||
app.source_workspace
|
||||
.edit(raw.clone(), app.source_editor.cursor());
|
||||
let task = app.update(Message::Home(home::Event::Walkthrough));
|
||||
assert!(matches!(app.pending, Some(Pending::Walkthrough)));
|
||||
assert!(!app.busy);
|
||||
assert!(iced_runtime::task::into_stream(task).is_none());
|
||||
assert_eq!(app.source_workspace.text(), raw);
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
let _ = app.update(Message::Escape);
|
||||
assert!(app.pending.is_none());
|
||||
assert_eq!(app.source_workspace.text(), raw);
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
}
|
||||
|
||||
fn retained_snapshot(app: &App) -> recovery::Snapshot {
|
||||
recovery::Snapshot::capture(
|
||||
app.canvas_document,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//! A view picker follows a view inspector without stealing semantic selection.
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (App, Id, Id, Id) {
|
||||
let project = language::parse(
|
||||
r#"arcadia 6; project views "View switching" {
|
||||
oa_activity work "Inspect work" {}
|
||||
diagram first "Original concern" {}
|
||||
diagram second "Commands review" {}
|
||||
node original_work { diagram @first; element @work; }
|
||||
node copied_work { diagram @second; element @work; }
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let id = |alias| {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, value)| *value == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
};
|
||||
let (first, second, work) = (id("first"), id("second"), id("work"));
|
||||
let mut app = App::with_project(project);
|
||||
app.page = Page::Diagram;
|
||||
app.active_diagram = Some(second);
|
||||
app.selected = second;
|
||||
app.refresh_draft();
|
||||
(app, first, second, work)
|
||||
}
|
||||
fn choose(app: &mut App, id: Id) {
|
||||
let _ = app.update(Message::Diagram(Choice {
|
||||
id,
|
||||
label: "Captured choice".into(),
|
||||
}));
|
||||
}
|
||||
fn field(app: &mut App, path: &str, value: &str) {
|
||||
let _ = app.update(Message::Field(path.into(), value.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_visible_view_follows_its_clean_inspector_without_a_model_edit() {
|
||||
let (mut app, original, copy, _) = fixture();
|
||||
let before = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
choose(&mut app, original);
|
||||
assert_eq!(app.active_diagram, Some(original));
|
||||
assert_eq!(app.selected, original);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, original);
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().inputs["/name"].raw,
|
||||
"Original concern"
|
||||
);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(!app.document.can_undo());
|
||||
field(&mut app, "/name", "Edited visible original");
|
||||
assert!(app.apply());
|
||||
assert_eq!(
|
||||
app.project().diagrams[&original].name,
|
||||
"Edited visible original"
|
||||
);
|
||||
assert_eq!(app.project().diagrams[©].name, "Commands review");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_view_draft_blocks_the_switch_and_retains_raw_input() {
|
||||
let (mut app, original, copy, _) = fixture();
|
||||
field(&mut app, "/id", "-");
|
||||
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
let before = app.document.snapshot();
|
||||
choose(&mut app, original);
|
||||
assert_eq!(app.active_diagram, Some(copy));
|
||||
assert_eq!(app.selected, copy);
|
||||
assert_eq!(
|
||||
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
raw
|
||||
);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_pending_view_edit_applies_to_old_view_then_inspects_the_new_view() {
|
||||
let (mut app, original, copy, _) = fixture();
|
||||
let before = app.document.snapshot();
|
||||
field(&mut app, "/name", "Reviewed copy");
|
||||
choose(&mut app, original);
|
||||
assert_eq!(app.project().diagrams[©].name, "Reviewed copy");
|
||||
assert_eq!(
|
||||
app.project().diagrams[&original],
|
||||
before.decoded.project.diagrams[&original]
|
||||
);
|
||||
assert_eq!(app.selected, original);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, original);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_uuid_edit_follows_the_same_view_or_a_different_picker_target() {
|
||||
for same_view in [false, true] {
|
||||
let (mut app, original, copy, _) = fixture();
|
||||
let changed = Id::from_u128(0xabcd123440008000000000000001);
|
||||
field(&mut app, "/id", &changed.to_string());
|
||||
choose(&mut app, if same_view { copy } else { original });
|
||||
let expected = if same_view { changed } else { original };
|
||||
assert!(!app.project().diagrams.contains_key(©));
|
||||
assert!(app.project().diagrams.contains_key(&changed));
|
||||
assert_eq!(app.active_diagram, Some(expected));
|
||||
assert_eq!(app.selected, expected);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, expected);
|
||||
assert!(!app.draft_dirty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_switch_preserves_an_independent_semantic_inspection() {
|
||||
let (mut app, original, _, work) = fixture();
|
||||
let _ = app.update(Message::Select(work));
|
||||
let before = app.document.snapshot();
|
||||
choose(&mut app, original);
|
||||
assert_eq!(app.active_diagram, Some(original));
|
||||
assert_eq!(app.selected, work);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, work);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_diagram_choice_never_applies_an_unrelated_valid_draft() {
|
||||
let (mut app, _, copy, _) = fixture();
|
||||
field(&mut app, "/name", "Pending copy name");
|
||||
let before = app.document.snapshot();
|
||||
choose(&mut app, Id::new_v4());
|
||||
assert_eq!(app.active_diagram, Some(copy));
|
||||
assert_eq!(app.selected, copy);
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().inputs["/name"].raw,
|
||||
"Pending copy name"
|
||||
);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
assert!(app.status.contains("no longer exists"));
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Accepted semantic review keeps selection, layer and visible diagram coherent.
|
||||
use super::*;
|
||||
use crate::{
|
||||
semantic_review::{RowEvent, State},
|
||||
workflow::{Event, Workflow},
|
||||
};
|
||||
|
||||
fn send(app: &mut App, message: Message) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
fn state(app: &App) -> &State {
|
||||
let Some(Workflow::Semantic(state)) = &app.workflow else {
|
||||
panic!("{}", app.status)
|
||||
};
|
||||
state
|
||||
}
|
||||
fn row_event(app: &mut App, event: RowEvent) {
|
||||
let event = state(app).address_event(event);
|
||||
send(app, Message::Workflow(Event::Semantic(event)));
|
||||
}
|
||||
fn fixture(reverse: bool, destination_view: bool) -> (App, Id, Id, Id, Option<Id>) {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Confirm stable pressure", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Confirm safe pressure", None)
|
||||
.unwrap();
|
||||
let trace = Id::new_v4();
|
||||
p.traces.insert(
|
||||
trace,
|
||||
Trace {
|
||||
id: trace,
|
||||
alias: "confirm_trace".into(),
|
||||
name: "Confirmation refinement".into(),
|
||||
description: String::new(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
let (source, target, source_layer, target_layer) = if reverse {
|
||||
(sa, oa, Layer::Sa, Layer::Oa)
|
||||
} else {
|
||||
(oa, sa, Layer::Oa, Layer::Sa)
|
||||
};
|
||||
let from = p.ensure_diagram(source_layer);
|
||||
p.place(from, source, Position { x: 91., y: 143. });
|
||||
let to = destination_view.then(|| {
|
||||
let view = p.ensure_diagram(target_layer);
|
||||
p.place(view, target, Position { x: 487., y: 213. });
|
||||
view
|
||||
});
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = source_layer;
|
||||
app.active_diagram = Some(from);
|
||||
app.selected = source;
|
||||
app.refresh_draft();
|
||||
(app, source, target, from, to)
|
||||
}
|
||||
fn name_take(app: &mut App, source: Id) -> Project {
|
||||
send(app, Message::StartSemanticReview(source));
|
||||
let facet = FacetSelector::Header(HeaderFacet::Name);
|
||||
row_event(app, RowEvent::Select(facet.clone()));
|
||||
row_event(
|
||||
app,
|
||||
RowEvent::Decide(facet, Some(SemanticDecision::TakeSource)),
|
||||
);
|
||||
assert!(state(app).can_apply(), "{:?}", state(app).error);
|
||||
let Some(Workflow::Semantic(state)) = &mut app.workflow else {
|
||||
unreachable!()
|
||||
};
|
||||
let command = state.command(app.document.project()).unwrap();
|
||||
execute_batch(app.document.project(), &[command]).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_name_take_selects_the_counterpart_view_in_both_directions_with_one_exact_history_entry()
|
||||
{
|
||||
for reverse in [false, true] {
|
||||
let (mut app, source, target, from, to) = fixture(reverse, true);
|
||||
app.search = format!("@{}", app.project().elements[&source].alias);
|
||||
let query = app.search.clone();
|
||||
let before = app.document.snapshot();
|
||||
let expected = name_take(&mut app, source);
|
||||
app.selected_node = app.project().diagrams[&from].nodes.keys().next().copied();
|
||||
send(&mut app, Message::Workflow(Event::Apply));
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.project().diagrams, before.decoded.project.diagrams);
|
||||
assert_eq!(app.selected, target);
|
||||
assert_eq!(app.layer, app.project().elements[&target].layer());
|
||||
assert_eq!(app.active_diagram, to);
|
||||
assert_eq!(
|
||||
app.project().diagrams[&app.active_diagram.unwrap()].layer,
|
||||
app.layer
|
||||
);
|
||||
// refresh_draft deliberately selects the target's visible placement.
|
||||
// The invariant is that the old-layer placement cannot survive.
|
||||
let placement = app.selected_node.expect("visible counterpart placement");
|
||||
assert_eq!(
|
||||
app.project().diagrams[&to.unwrap()].nodes[&placement].element,
|
||||
target
|
||||
);
|
||||
assert!(!app.project().diagrams[&from].nodes.contains_key(&placement));
|
||||
assert_eq!(app.page, Page::Diagram);
|
||||
assert_eq!(app.search, query);
|
||||
assert!(app.workflow.is_none());
|
||||
assert_eq!(app.document.revision(), 1);
|
||||
let 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());
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &accepted.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &accepted.bundle);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_review_without_a_destination_view_clears_the_old_layer_canvas_without_creating_a_view()
|
||||
{
|
||||
let (mut app, source, target, _, _) = fixture(false, false);
|
||||
let expected = name_take(&mut app, source);
|
||||
send(&mut app, Message::Workflow(Event::Apply));
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.selected, target);
|
||||
assert_eq!(app.layer, Layer::Sa);
|
||||
assert_eq!(app.active_diagram, None);
|
||||
assert_eq!(app.project().diagrams.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_already_showing_the_target_keeps_its_camera_and_query() {
|
||||
let (mut app, source, target, _, to) = fixture(false, true);
|
||||
app.layer = Layer::Sa;
|
||||
app.active_diagram = to;
|
||||
app.selected = target;
|
||||
app.zoom = 1.31;
|
||||
app.pan = iced::Vector::new(-219., 88.);
|
||||
app.search = "pressure".into();
|
||||
app.refresh_draft();
|
||||
let expected = name_take(&mut app, source);
|
||||
send(&mut app, Message::Workflow(Event::Apply));
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.active_diagram, to);
|
||||
assert_eq!(app.zoom, 1.31);
|
||||
assert_eq!(app.pan, iced::Vector::new(-219., 88.));
|
||||
assert_eq!(app.search, "pressure");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_semantic_source_writeback_retains_review_camera_and_original_navigation() {
|
||||
let (mut app, source, _, from, _) = fixture(false, true);
|
||||
let bundle = app.document.bundle().clone();
|
||||
let mut limits = source_project::ProjectLimits::default();
|
||||
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::<usize>() + 1;
|
||||
app.document = Document::from_bundle(bundle, limits).unwrap();
|
||||
app.zoom = 1.21;
|
||||
app.pan = iced::Vector::new(72., -44.);
|
||||
app.search = "confirmation".into();
|
||||
let before = app.document.snapshot();
|
||||
let _valid_model = name_take(&mut app, source);
|
||||
send(&mut app, Message::Workflow(Event::Apply));
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert_eq!(app.selected, source);
|
||||
assert_eq!(app.layer, Layer::Oa);
|
||||
assert_eq!(app.active_diagram, Some(from));
|
||||
assert_eq!(app.page, Page::Diagram);
|
||||
assert_eq!(app.zoom, 1.21);
|
||||
assert_eq!(app.pan, iced::Vector::new(72., -44.));
|
||||
assert_eq!(app.search, "confirmation");
|
||||
assert!(state(&app).error.is_some());
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
//! 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()));
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//! App-level copy behavior, with separate state/architecture view identities.
|
||||
use super::*;
|
||||
|
||||
struct Fixture {
|
||||
app: App,
|
||||
architecture: Id,
|
||||
states: Id,
|
||||
machine: Id,
|
||||
region: Id,
|
||||
state: Id,
|
||||
}
|
||||
|
||||
fn fixture() -> Fixture {
|
||||
let project = language::parse(
|
||||
r#"arcadia 3; project copied_views "Copied views" {
|
||||
oa_entity crew "Crew" {}
|
||||
oa_state_machine modes "Modes" { parent @crew; }
|
||||
oa_region main "Main" { parent @modes; }
|
||||
oa_state ready "Ready" { parent @main; }
|
||||
diagram architecture "Architecture" {}
|
||||
diagram state_view "Modes view" { projection { StateMachine { machine @modes; }; }; }
|
||||
node crew_view { diagram @architecture; element @crew; }
|
||||
node main_view { diagram @state_view; element @main; }
|
||||
node ready_view { diagram @state_view; element @ready; }
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let identity = |alias| {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, candidate)| *candidate == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
};
|
||||
let architecture = identity("architecture");
|
||||
let states = identity("state_view");
|
||||
let machine = identity("modes");
|
||||
let region = identity("main");
|
||||
let state = identity("ready");
|
||||
let mut app = App::with_project(project);
|
||||
// Initial view UUID order must not determine the test's workspace.
|
||||
app.page = Page::Behavior;
|
||||
app.active_diagram = Some(architecture);
|
||||
app.behavior.active_owner = Some(machine);
|
||||
app.behavior.diagram.diagram = Some(states);
|
||||
app.selected = state;
|
||||
app.refresh_draft();
|
||||
Fixture {
|
||||
app,
|
||||
architecture,
|
||||
states,
|
||||
machine,
|
||||
region,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
fn send(app: &mut App, message: Message) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
|
||||
fn raw_property(app: &App) -> serde_json::Value {
|
||||
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copying_the_current_state_view_uses_its_own_identity_and_is_one_source_undo() {
|
||||
let mut f = fixture();
|
||||
assert_eq!(f.app.state_context().unwrap().diagram, f.states);
|
||||
assert_eq!(f.app.active_diagram, Some(f.architecture));
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
send(&mut f.app, Message::DuplicateDiagram(f.states));
|
||||
assert_eq!(f.app.page, Page::Behavior);
|
||||
assert_eq!(f.app.behavior.active_owner, Some(f.machine));
|
||||
let copied = f.app.state_context().unwrap().diagram;
|
||||
assert_ne!(copied, f.states);
|
||||
assert_ne!(copied, f.architecture);
|
||||
assert_eq!(f.app.active_diagram, Some(copied));
|
||||
assert_eq!(f.app.selected, copied);
|
||||
assert_eq!(
|
||||
f.app.project().diagrams[&copied].projection.state_machine(),
|
||||
Some(f.machine)
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.project().diagrams[&copied]
|
||||
.nodes
|
||||
.values()
|
||||
.map(|node| node.element)
|
||||
.collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from([f.region, f.state])
|
||||
);
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
let accepted = f.app.document.snapshot();
|
||||
assert_eq!(
|
||||
source_project::compile_bundle(f.app.document.bundle(), LIMITS)
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
*f.app.project()
|
||||
);
|
||||
let mut only_original = f.app.project().clone();
|
||||
only_original.diagrams.remove(&copied);
|
||||
assert_eq!(only_original, before.decoded.project);
|
||||
send(&mut f.app, Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
send(&mut f.app, Message::Redo);
|
||||
assert_eq!(f.app.project(), &accepted.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &accepted.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copying_architecture_keeps_architecture_even_with_a_retained_state_view() {
|
||||
let mut f = fixture();
|
||||
f.app.page = Page::Diagram;
|
||||
f.app.active_diagram = Some(f.architecture);
|
||||
f.app.selected = f.architecture;
|
||||
f.app.refresh_draft();
|
||||
send(&mut f.app, Message::DuplicateDiagram(f.architecture));
|
||||
let copied = f.app.active_diagram.unwrap();
|
||||
assert_ne!(copied, f.architecture);
|
||||
assert_eq!(f.app.page, Page::Diagram);
|
||||
assert!(f.app.state_context().is_none());
|
||||
assert_eq!(f.app.project().diagrams[&copied].name, "Architecture copy");
|
||||
assert_eq!(
|
||||
f.app.project().diagrams[&copied].projection,
|
||||
f.app.project().diagrams[&f.architecture].projection
|
||||
);
|
||||
assert_eq!(f.app.project().diagrams[&copied].nodes.len(), 1);
|
||||
assert_eq!(f.app.selected, copied);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_copy_does_not_apply_an_unrelated_valid_property_draft() {
|
||||
let mut f = fixture();
|
||||
send(
|
||||
&mut f.app,
|
||||
Message::Field("/name".into(), "Pending state rename".into()),
|
||||
);
|
||||
let raw = raw_property(&f.app);
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
send(&mut f.app, Message::DuplicateDiagram(Id::new_v4()));
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert_eq!(raw_property(&f.app), raw);
|
||||
assert_eq!(f.app.selected, f.state);
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.status.contains("no longer exists"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_rejection_preserves_invalid_property_and_pending_behavior_drafts() {
|
||||
let mut f = fixture();
|
||||
send(
|
||||
&mut f.app,
|
||||
Message::Field("/alias".into(), "invalid alias!".into()),
|
||||
);
|
||||
let before = f.app.document.snapshot();
|
||||
let raw = raw_property(&f.app);
|
||||
send(&mut f.app, Message::DuplicateDiagram(f.states));
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(raw_property(&f.app), raw);
|
||||
assert!(!f.app.document.can_undo());
|
||||
|
||||
let mut f = fixture();
|
||||
send(
|
||||
&mut f.app,
|
||||
Message::Behavior(crate::behavior::Event::States(
|
||||
crate::behavior::states::Event::Start(
|
||||
f.machine,
|
||||
crate::behavior::states::Tool::Vertex,
|
||||
f.region,
|
||||
),
|
||||
)),
|
||||
);
|
||||
send(
|
||||
&mut f.app,
|
||||
Message::Behavior(crate::behavior::Event::States(
|
||||
crate::behavior::states::Event::Name("Unfinished nested state".into()),
|
||||
)),
|
||||
);
|
||||
assert!(f.app.behavior.has_pending());
|
||||
let raw = f.app.behavior.recovery_snapshot().unwrap();
|
||||
let before = f.app.document.snapshot();
|
||||
send(&mut f.app, Message::DuplicateDiagram(f.states));
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.behavior.recovery_snapshot().unwrap(), raw);
|
||||
assert_eq!(f.app.state_context().unwrap().diagram, f.states);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_rejection_preserves_an_incomplete_source_buffer() {
|
||||
let mut f = fixture();
|
||||
let raw = "arcadia 6; project unfinished \"Unfinished source\" {".to_owned();
|
||||
send(
|
||||
&mut f.app,
|
||||
Message::SourceEdit(text_editor::Action::SelectAll),
|
||||
);
|
||||
send(
|
||||
&mut f.app,
|
||||
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste(
|
||||
Arc::new(raw.clone()),
|
||||
))),
|
||||
);
|
||||
assert!(f.app.source_dirty());
|
||||
let before = f.app.document.snapshot();
|
||||
let source = f.app.source_workspace.bundle();
|
||||
send(&mut f.app, Message::DuplicateDiagram(f.states));
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.source_workspace.bundle(), source);
|
||||
assert_eq!(f.app.source_workspace.text(), raw);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
Reference in New Issue
Block a user