Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/contextual_source_tests.rs
T

1331 lines
48 KiB
Rust

use super::{creation, scenario, source_draft as source, topology};
use crate::document::Document;
use oa_core::source_project::ProjectLimits;
use oa_core::*;
fn id(value: &serde_json::Value, field: &str) -> Id {
serde_json::from_value(value[field].clone()).unwrap()
}
fn child(value: &serde_json::Value, edit: source::Edit) -> source::Event {
source::Event::Edit(id(&value["source"]["editing"], "token"), edit)
}
fn scenario_project(layer: Layer, viewpoint: ScenarioViewpoint) -> (Project, Id) {
let mut p = Project::empty();
let owner = p
.add_element(Kind::Scenario, layer, "Emergency coordination", None)
.unwrap();
if let Some(SharedBody::Scenario { viewpoint: v, .. }) =
p.elements.get_mut(&owner).unwrap().shared_mut()
{
*v = viewpoint;
}
(p, owner)
}
fn scenario_source(s: &mut scenario::State, p: &Project, name: &str, kind: Option<Kind>) {
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
p,
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
if let Some(kind) = kind {
s.update(
p,
scenario::Event::Source(child(&raw, source::Edit::Kind(kind))),
)
.unwrap();
}
s.update(
p,
scenario::Event::Source(child(&raw, source::Edit::Name(name.into()))),
)
.unwrap();
s.update(p, scenario::Event::Source(child(&raw, source::Edit::Use)))
.unwrap();
}
fn history(p: &Project, command: Command) -> Project {
let expected = execute_batch(p, std::slice::from_ref(&command)).unwrap();
let mut doc = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
let before = doc.bundle().clone();
doc.execute(command).unwrap();
assert_eq!(doc.project(), &expected);
let after = doc.bundle().clone();
assert!(doc.undo());
assert_eq!(doc.project(), p);
assert_eq!(doc.bundle(), &before);
assert!(!doc.can_undo());
assert!(doc.redo());
assert_eq!(doc.project(), &expected);
assert_eq!(doc.bundle(), &after);
assert_eq!(
Document::from_bundle(after, ProjectLimits::default())
.unwrap()
.project(),
&expected
);
expected
}
#[test]
fn contextual_source_lifelines_stage_exact_separate_records_and_one_history_entry() {
for (layer, viewpoint, kind) in [
(
Layer::Oa,
ScenarioViewpoint::OperationalActivity,
Kind::Activity,
),
(Layer::Sa, ScenarioViewpoint::Functional, Kind::Function),
(
Layer::Oa,
ScenarioViewpoint::ParticipantInteraction,
Kind::Role,
),
(
Layer::Sa,
ScenarioViewpoint::BoundaryInterface,
Kind::SystemActor,
),
] {
let (p, owner) = scenario_project(layer, viewpoint);
let before = p.clone();
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
s.update(&p, scenario::Event::Name("Assessment role 雨".into()))
.unwrap();
scenario_source(&mut s, &p, "Assess distress 雨", Some(kind));
assert_eq!(p, before);
let raw = s.recovery_snapshot().unwrap().unwrap();
let command = s.prepare_pending(&p).unwrap();
assert_eq!(command, s.prepare_pending(&p).unwrap());
let recovered = scenario::State::from_recovery(raw).unwrap();
assert_eq!(command, recovered.prepare_pending(&p).unwrap());
let selected = s.created_selection(&p, &command).unwrap();
let result = history(&p, command);
assert_eq!(result.elements.len(), p.elements.len() + 2);
assert_eq!(result.relations, p.relations);
assert_eq!(result.diagrams, p.diagrams);
let line = &result.elements[&selected];
assert_eq!(line.name, "Assessment role 雨");
assert_eq!(line.parent, Some(owner));
let Some(SharedBody::Lifeline {
represented: Some(represented),
}) = line.shared()
else {
panic!("lifeline")
};
let work = &result.elements[represented];
assert_eq!(work.name, "Assess distress 雨");
assert_eq!(work.kind(), kind);
assert_eq!(work.parent, None);
}
}
#[test]
fn contextual_source_default_consumer_name_has_stable_alias_and_correlated_selection() {
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
scenario_source(&mut s, &p, "Same source and consumer name", None);
let command = s.prepare_pending(&p).unwrap();
assert_eq!(command, s.prepare_pending(&p).unwrap());
let selected = s.created_selection(&p, &command).unwrap();
let result = execute_batch(&p, std::slice::from_ref(&command)).unwrap();
assert_eq!(
result.elements[&selected].name,
"Same source and consumer name"
);
let unrelated = Command::Batch(vec![command, Command::Rename(selected, "Other".into())]);
assert!(s.created_selection(&p, &unrelated).is_none());
}
#[test]
fn contextual_source_back_retains_parent_and_rejects_late_child_event() {
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
s.update(&p, scenario::Event::Name(" Parent name 雨\n".into()))
.unwrap();
let before = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
scenario::Event::Source(source::Event::Open(id(&before, "invocation"))),
)
.unwrap();
let open = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
scenario::Event::Source(child(&open, source::Edit::Name("discarded".into()))),
)
.unwrap();
s.update(
&p,
scenario::Event::Source(child(&open, source::Edit::Back)),
)
.unwrap();
let after = s.recovery_snapshot().unwrap().unwrap();
for field in ["name", "target", "owner", "parent", "before", "invocation"] {
assert_eq!(before[field], after[field]);
}
assert!(
s.update(&p, scenario::Event::Source(child(&open, source::Edit::Use)))
.is_err()
);
assert_eq!(s.recovery_snapshot().unwrap().unwrap(), after);
assert_eq!(p.elements.len(), 1);
}
#[test]
fn contextual_source_recovery_keeps_inner_raw_fields_and_refuses_deleted_or_changed_owner() {
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let open = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
scenario::Event::Source(child(&open, source::Edit::Name(" 未完成\n\0".into()))),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
let mut recovered = scenario::State::from_recovery(raw.clone()).unwrap();
assert!(recovered.source_open());
assert_eq!(recovered.recovery_snapshot().unwrap().unwrap(), raw);
p.elements.remove(&owner);
assert!(recovered.prepare_pending(&p).is_err());
assert!(
recovered
.update(&p, scenario::Event::Source(child(&raw, source::Edit::Use)))
.is_err()
);
assert_eq!(recovered.recovery_snapshot().unwrap().unwrap(), raw);
}
#[test]
fn contextual_source_existing_choice_prunes_staged_dependency() {
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let existing = p
.add_element(Kind::Activity, Layer::Oa, "Same name", None)
.unwrap();
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
scenario_source(&mut s, &p, "Same name", None);
s.update(
&p,
scenario::Event::Target(crate::app::Choice {
id: existing,
label: "Same name".into(),
}),
)
.unwrap();
let result = history(&p, s.prepare_pending(&p).unwrap());
assert_eq!(result.elements.len(), p.elements.len() + 1);
let line = result
.children(owner)
.find(|e| e.kind() == Kind::Lifeline)
.unwrap();
assert!(
matches!(line.shared(),Some(SharedBody::Lifeline{represented:Some(id)}) if *id==existing)
);
}
#[test]
fn contextual_source_machine_before_any_owner_creates_optional_view_atomically() {
for (layer, kind) in [(Layer::Oa, Kind::Actor), (Layer::Sa, Kind::SystemActor)] {
let p = Project::empty();
let mut s = creation::State::default();
s.update(&p, creation::Event::Open(layer)).unwrap();
s.update(&p, creation::Event::Choose(creation::Family::Machine))
.unwrap();
s.update(&p, creation::Event::Name("Operating modes 雨".into()))
.unwrap();
s.update(&p, creation::Event::FirstView(true)).unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
creation::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
creation::Event::Source(child(&raw, source::Edit::Kind(kind))),
)
.unwrap();
s.update(
&p,
creation::Event::Source(child(
&raw,
source::Edit::Name("Rescue participant 雨".into()),
)),
)
.unwrap();
s.update(&p, creation::Event::Source(child(&raw, source::Edit::Use)))
.unwrap();
let command = s.prepare_pending(&p).unwrap();
assert_eq!(command, s.prepare_pending(&p).unwrap());
let target = s.created_by(&p, &command).unwrap();
assert_eq!(
creation::State::from_recovery(s.recovery_snapshot().unwrap().unwrap())
.unwrap()
.prepare_pending(&p)
.unwrap(),
command
);
let result = history(&p, command);
assert_eq!(result.elements.len(), 2);
assert_eq!(result.diagrams.len(), 1);
let machine = &result.elements[&target.behavior];
assert_eq!(machine.kind(), Kind::StateMachine);
let owner = &result.elements[&machine.parent.unwrap()];
assert_eq!(owner.kind(), kind);
assert_eq!(owner.name, "Rescue participant 雨");
let diagram = &result.diagrams[&target.diagram.unwrap()];
assert_eq!(
diagram.projection,
ProjectionKind::StateMachine {
machine: target.behavior
}
);
assert_eq!(diagram.nodes.len(), 1);
assert_eq!(
diagram.nodes.values().next().unwrap().element,
target.behavior
);
}
}
#[test]
fn contextual_source_second_system_and_role_machine_owner_refuse_without_fallback() {
let mut p = Project::empty();
p.add_element(Kind::System, Layer::Sa, "Existing system", None)
.unwrap();
let mut source = source::State::default();
let scope = source::Scope::Machine { layer: Layer::Sa };
let invocation = Id::new_v4();
source
.update(&p, scope, invocation, source::Event::Open(invocation))
.unwrap();
let raw = serde_json::to_value(&source).unwrap();
let token = id(&raw["editing"], "token");
source
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Kind(Kind::System)),
)
.unwrap();
source
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Name("Illegal second system".into())),
)
.unwrap();
let before = serde_json::to_value(&source).unwrap();
assert!(
source
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Use)
)
.is_err()
);
assert_eq!(serde_json::to_value(&source).unwrap(), before);
assert!(
source
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Kind(Kind::Role))
)
.is_err()
);
}
#[test]
fn contextual_source_work_uses_reserved_contribution_and_link_ids_and_one_undo() {
for layer in [Layer::Oa, Layer::Sa] {
let mut p = Project::empty();
let owner = p
.add_element(
if layer == Layer::Oa {
Kind::Process
} else {
Kind::FunctionalChain
},
layer,
"Response path",
None,
)
.unwrap();
let mut s = topology::State::default();
s.update(
&p,
topology::Event::Start(owner, topology::Tool::Work, None),
)
.unwrap();
s.update(&p, topology::Event::Name("New contribution".into()))
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
topology::Event::Source(child(&raw, source::Edit::Name("New semantic work".into()))),
)
.unwrap();
s.update(&p, topology::Event::Source(child(&raw, source::Edit::Use)))
.unwrap();
let command = s.prepare_pending(&p).unwrap();
assert_eq!(command, s.prepare_pending(&p).unwrap());
assert_eq!(
topology::State::from_recovery(s.recovery_snapshot().unwrap().unwrap())
.unwrap()
.prepare_pending(&p)
.unwrap(),
command
);
let selected = s.created_selection(&p, &command).unwrap();
let result = history(&p, command);
assert_eq!(result.elements.len(), 3);
let occurrence = &result.elements[&selected];
assert_eq!(occurrence.parent, Some(owner));
assert_eq!(occurrence.name, "New contribution");
let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Action {
action: Some(work),
lifeline: None,
},
..
}) = occurrence.shared()
else {
panic!("Action contribution")
};
assert_eq!(result.elements[work].name, "New semantic work");
assert_eq!(result.elements[work].parent, None);
assert_eq!(result.elements[&owner].steps().unwrap(), &vec![selected]);
}
}
#[test]
fn contextual_source_failed_source_budget_preserves_both_drafts_and_accepted_bytes() {
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
scenario_source(&mut s, &p, &"Long raw semantic name 雨".repeat(40), None);
let initial = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
let bytes: usize = initial.bundle().files.values().map(String::len).sum();
let limits = ProjectLimits {
model: language::ParseLimits {
max_source_bytes: bytes + 16,
..language::ParseLimits::UNLIMITED
},
..ProjectLimits::default()
};
let mut doc = Document::from_bundle(initial.bundle().clone(), limits).unwrap();
let raw = s.recovery_snapshot().unwrap();
assert!(doc.execute(s.prepare_pending(&p).unwrap()).is_err());
assert_eq!(doc.project(), &p);
assert_eq!(doc.bundle(), initial.bundle());
assert!(!doc.can_undo());
assert_eq!(s.recovery_snapshot().unwrap(), raw);
}
#[test]
fn contextual_source_owner_first_can_return_to_an_unnamed_machine_without_losing_source() {
let p = Project::empty();
let mut s = creation::State::default();
s.update(&p, creation::Event::Open(Layer::Oa)).unwrap();
s.update(&p, creation::Event::Choose(creation::Family::Machine))
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
creation::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
creation::Event::Source(child(&raw, source::Edit::Kind(Kind::Actor))),
)
.unwrap();
s.update(
&p,
creation::Event::Source(child(
&raw,
source::Edit::Name("Owner entered first 雨".into()),
)),
)
.unwrap();
s.update(&p, creation::Event::Source(child(&raw, source::Edit::Use)))
.unwrap();
assert!(!s.source_open());
assert!(s.prepare_pending(&p).is_err());
let staged = s.recovery_snapshot().unwrap().unwrap();
s.update(&p, creation::Event::Name("Now name the machine".into()))
.unwrap();
let result = history(&p, s.prepare_pending(&p).unwrap());
let owner = id(&staged["source"]["staged"], "id");
assert_eq!(result.named(owner), "Owner entered first 雨");
}
#[test]
fn contextual_source_inserts_work_on_the_exact_guarded_link_and_refuses_exchange_identity_conflict()
{
let mut f = topology::direct_tests::fixture(Layer::Sa);
let start = |p: &Project| {
let mut s = topology::State::default();
s.begin(
p,
topology::direct_tests::context(f.owner),
topology::Tool::Work,
ControlKind::Choice,
topology::Insertion {
link: Some(f.link),
before: Some(f.nodes[1]),
},
)
.unwrap();
s
};
let mut s = start(&f.p);
s.update(
&f.p,
topology::Event::Name(" New contribution 雨 ".into()),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&f.p,
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&f.p,
topology::Event::Source(child(&raw, source::Edit::Name("Inserted function".into()))),
)
.unwrap();
s.update(
&f.p,
topology::Event::Source(child(&raw, source::Edit::Use)),
)
.unwrap();
let command = s.prepare_pending(&f.p).unwrap();
assert_eq!(command, s.prepare_pending(&f.p).unwrap());
let selected = s.created_selection(&f.p, &command).unwrap();
let result = history(&f.p, command);
let link = &result.relations[&f.link];
assert_eq!(link.source(), f.nodes[0]);
assert_eq!(link.target(), selected);
assert!(matches!(&link.body,RelationBody::SequenceLink{guard,..} if guard=="approved"));
assert_eq!(result.elements[&selected].name, " New contribution 雨 ");
assert!(
result
.relations
.values()
.any(|r| r.kind() == RelationKind::SequenceLink
&& r.source() == selected
&& r.target() == f.nodes[1])
);
for work in f.work {
assert_eq!(result.elements[&work], f.p.elements[&work]);
}
// The preceding exchange fixes its target work identity. A new source cannot match it.
if let Some(SharedBody::Occurrence { body, .. }) =
f.p.elements.get_mut(&f.nodes[0]).unwrap().shared_mut()
{
*body = OccurrenceBody::Exchange {
exchange: Some(f.exchange),
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
reply_to: None,
values: vec![],
operation: None,
};
}
let mut s = start(&f.p);
s.update(&f.p, topology::Event::Name("Retain this slot".into()))
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&f.p,
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&f.p,
topology::Event::Source(child(
&raw,
source::Edit::Name("Cannot replace exchange endpoint".into()),
)),
)
.unwrap();
let before = s.recovery_snapshot().unwrap();
assert!(
s.update(
&f.p,
topology::Event::Source(child(&raw, source::Edit::Use))
)
.is_err()
);
assert_eq!(s.recovery_snapshot().unwrap(), before);
assert!(s.source_open());
}
#[test]
fn contextual_source_app_empty_actions_and_child_open_refuse_other_raw_editors() {
use crate::app::{App, Message, Page};
for property in [true, false] {
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut app = App::with_project(p);
app.page = Page::Behavior;
if property {
app.draft
.as_mut()
.unwrap()
.input("/name", "Valid unapplied name".into());
} else {
app.source_workspace.edit(
format!("{}\n// retained draft\n", app.source_workspace.text()),
app.source_editor.cursor(),
);
}
let project = app.project().clone();
let bundle = app.document.bundle().clone();
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
let workspace = serde_json::to_value(app.source_workspace.recovery_snapshot()).unwrap();
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Start(owner, scenario::Tool::Lifeline),
)));
assert!(!app.behavior.has_pending());
assert_eq!(app.project(), &project);
assert_eq!(app.document.bundle(), &bundle);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert_eq!(
serde_json::to_value(app.source_workspace.recovery_snapshot()).unwrap(),
workspace
);
assert!(!app.document.can_undo());
}
}
#[test]
fn contextual_source_app_modal_escape_then_child_back_then_parent_cancel_preserve_accepted_source()
{
use crate::app::{App, Message, Page, Pending};
for file in [false, true] {
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut app = App::with_project(p.clone());
app.page = Page::Behavior;
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Start(owner, scenario::Tool::Lifeline),
)));
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Name("Keep parent name 雨".into()),
)));
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)));
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Source(child(
&raw,
source::Edit::Name("Uncommitted source 雨".into()),
)),
)));
let before = app.behavior.recovery_snapshot().unwrap();
let bundle = app.document.bundle().clone();
if file {
app.file_prompt = Some(crate::app::FilePrompt {
operation: crate::app::FileOperation::Save,
path: "Unsubmitted.arcadia".into(),
overwrite: false,
});
} else {
app.pending = Some(Pending::Walkthrough);
}
let _ = app.update(Message::Escape);
assert_eq!(app.behavior.recovery_snapshot().unwrap(), before);
assert!(app.file_prompt.is_none());
assert!(app.pending.is_none());
let _ = app.update(Message::Escape);
assert!(!app.behavior.source_open());
assert!(app.behavior.has_pending());
assert_eq!(
app.behavior.scenario.recovery_snapshot().unwrap().unwrap()["name"],
"Keep parent name 雨"
);
let _ = app.update(Message::Escape);
assert!(!app.behavior.has_pending());
assert_eq!(app.project(), &p);
assert_eq!(app.document.bundle(), &bundle);
assert!(!app.document.can_undo());
}
}
#[test]
fn contextual_source_app_checkpoint_restores_inner_raw_draft_without_model_adoption() {
use crate::app::{App, Message, Page};
use std::sync::Arc;
let (p, owner) = scenario_project(Layer::Sa, ScenarioViewpoint::Functional);
let mut app = App::with_project(p.clone());
app.page = Page::Behavior;
app.layer = Layer::Sa;
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Start(owner, scenario::Tool::Lifeline),
)));
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)));
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Source(child(&raw, source::Edit::Name(" exact 未完成\n".into()))),
)));
let raw = app.behavior.recovery_snapshot().unwrap();
let snapshot = crate::recovery::Snapshot::capture(
Id::new_v4(),
&app.document,
&app.source_workspace,
app.draft.as_ref(),
&app.behavior,
None,
owner,
)
.unwrap();
let mut recovered = App::with_project(Project::empty());
let task = recovered.update(Message::CheckpointLoaded(Ok(Arc::new(snapshot))));
assert_eq!(recovered.project(), &p);
assert_eq!(recovered.layer, Layer::Sa);
assert_eq!(recovered.page, Page::Behavior);
assert!(recovered.behavior.source_open());
assert_eq!(recovered.behavior.recovery_snapshot().unwrap(), raw);
assert!(!recovered.document.can_undo());
assert!(
iced_runtime::task::into_stream(task).is_some(),
"Recovery preserves its lint/focus task"
);
}
#[test]
fn contextual_source_mounted_small_editor_keeps_back_and_use_outside_field_scroll() {
use iced::advanced::{Layout, layout, widget};
use iced::{Rectangle, Size, Vector};
struct Geometry {
actions: Option<Rectangle>,
fields: Option<Rectangle>,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if id == Some(&widget::Id::new("behavior-source-actions")) {
self.actions = Some(bounds);
}
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
_: Rectangle,
_: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if id == Some(&widget::Id::new("behavior-source-fields")) {
self.fields = Some(bounds);
}
}
}
let p = Project::empty();
let scope = source::Scope::Machine { layer: Layer::Oa };
let invocation = Id::new_v4();
let mut state = source::State::default();
state
.update(&p, scope, invocation, source::Event::Open(invocation))
.unwrap();
for size in [Size::new(340., 320.), Size::new(500., 570.)] {
let mut view: iced::Element<'_, source::Event, iced::Theme, ()> = state.view(&p).unwrap();
let mut tree = widget::Tree::new(view.as_widget());
let node =
view.as_widget_mut()
.layout(&mut tree, &(), &layout::Limits::new(Size::ZERO, size));
let mut geometry = Geometry {
actions: None,
fields: None,
};
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &(), &mut geometry);
let actions = geometry.actions.unwrap();
let fields = geometry.fields.unwrap();
assert!(
actions.x >= 0.
&& actions.y >= 0.
&& actions.x + actions.width <= size.width
&& actions.y + actions.height <= size.height
);
assert!(fields.height >= 100. && fields.y + fields.height <= actions.y);
}
}
#[test]
fn contextual_source_back_remains_available_after_owner_deletion_and_with_dirty_editors() {
use crate::app::{App, Message, Page};
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut state = scenario::State::default();
state
.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
state
.update(&p, scenario::Event::Name("Retain parent".into()))
.unwrap();
let raw = state.recovery_snapshot().unwrap().unwrap();
state
.update(
&p,
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let open = state.recovery_snapshot().unwrap().unwrap();
p.elements.remove(&owner);
state
.update(
&p,
scenario::Event::Source(child(&open, source::Edit::Back)),
)
.unwrap();
assert!(!state.source_open());
assert_eq!(
state.recovery_snapshot().unwrap().unwrap()["name"],
"Retain parent"
);
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut app = App::with_project(p.clone());
app.page = Page::Behavior;
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Start(owner, scenario::Tool::Lifeline),
)));
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)));
let open = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
app.draft
.as_mut()
.unwrap()
.input("/name", "Other raw input".into());
let property = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
let _ = app.update(Message::Behavior(super::Event::Scenario(
scenario::Event::Source(child(&open, source::Edit::Back)),
)));
assert!(!app.behavior.source_open());
assert!(app.behavior.has_pending());
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
property
);
assert_eq!(app.project(), &p);
assert!(!app.document.can_undo());
}
#[test]
fn contextual_source_incomplete_recovered_kind_reports_error_without_panicking() {
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
scenario_source(&mut s, &p, "Recovered source", None);
let mut raw = s.recovery_snapshot().unwrap().unwrap();
let mut wrong_scope = raw.clone();
wrong_scope["source"]["staged"]["scope"] =
serde_json::to_value(source::Scope::Machine { layer: Layer::Oa }).unwrap();
assert!(
scenario::State::from_recovery(wrong_scope)
.unwrap()
.prepare_pending(&p)
.unwrap_err()
.contains("different consumer context")
);
raw["source"]["staged"]["kind"] = serde_json::Value::Null;
let recovered = scenario::State::from_recovery(raw.clone()).unwrap();
assert!(recovered.prepare_pending(&p).is_err());
let selected = serde_json::from_value(raw["target"].clone()).unwrap();
let nested: source::State = serde_json::from_value(raw["source"].clone()).unwrap();
assert!(nested.label(selected).unwrap().contains("Choose kind"));
assert_eq!(recovered.recovery_snapshot().unwrap().unwrap(), raw);
}
#[test]
fn contextual_source_empty_palette_lifeline_cannot_open_a_previous_owner_or_layer() {
use crate::app::{App, Message, Page};
let (mut p, a) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let b = p
.add_element(Kind::Scenario, Layer::Oa, "Second scenario", None)
.unwrap();
let mut app = App::with_project(p.clone());
app.page = Page::Behavior;
app.layer = Layer::Oa;
app.behavior.active_owner = Some(a);
app.selected = a;
let context = app.scenario_context().unwrap();
let source = app.document.bundle().clone();
let begin = |context| scenario::CanvasEvent::BeginOccurrence {
context,
tool: scenario::Tool::Lifeline,
slot: scenario::Slot {
parent: a,
before: None,
},
lifeline: None,
};
app.behavior.active_owner = Some(b);
app.selected = b;
let _ = app.update(Message::ScenarioCanvas(begin(context)));
assert!(!app.behavior.has_pending());
assert_eq!(app.project(), &p);
app.behavior.active_owner = Some(a);
app.selected = a;
app.layer = Layer::Sa;
let _ = app.update(Message::ScenarioCanvas(begin(context)));
assert!(!app.behavior.has_pending());
app.layer = Layer::Oa;
let current = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(begin(current)));
assert_eq!(app.behavior.pending_owner(), Some(a));
assert_eq!(app.project(), &p);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}
#[test]
fn contextual_source_deleted_owner_view_retains_draft_instead_of_panicking_or_using_fallback() {
use crate::app::{App, Page};
for fallback in [false, true] {
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
if fallback {
p.add_element(
Kind::Scenario,
Layer::Oa,
"Different remaining scenario",
None,
)
.unwrap();
}
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
s.update(&p, scenario::Event::Name("Recovered consumer".into()))
.unwrap();
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let open = s.recovery_snapshot().unwrap().unwrap();
p.elements.remove(&owner);
let mut app = App::with_project(p.clone());
app.page = Page::Behavior;
app.layer = Layer::Oa;
app.behavior.scenario = scenario::State::from_recovery(open.clone()).unwrap();
drop(super::view(&app));
assert!(app.behavior.source_open());
assert_eq!(
app.behavior.scenario.recovery_snapshot().unwrap().unwrap(),
open
);
assert_eq!(app.project(), &p);
assert!(app.behavior.source_back());
drop(super::view(&app));
assert!(app.behavior.has_pending());
assert_eq!(
app.behavior.scenario.recovery_snapshot().unwrap().unwrap()["name"],
"Recovered consumer"
);
}
}
#[test]
fn contextual_source_nested_semantic_parent_is_explicit_and_revalidated() {
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let parent = p
.add_element(Kind::Activity, Layer::Oa, "Parent activity", None)
.unwrap();
let wrong = p
.add_element(
Kind::Entity,
Layer::Oa,
"Participant is not work ownership",
None,
)
.unwrap();
let scope = source::Scope::Lifeline {
owner,
viewpoint: ScenarioViewpoint::OperationalActivity,
};
let invocation = Id::new_v4();
let mut state = source::State::default();
state
.update(&p, scope, invocation, source::Event::Open(invocation))
.unwrap();
let raw = serde_json::to_value(&state).unwrap();
let token = id(&raw["editing"], "token");
assert!(
state
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Parent(Some(wrong)))
)
.is_err()
);
state
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Parent(Some(parent))),
)
.unwrap();
state
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Name("Explicit child work".into())),
)
.unwrap();
let new = state
.update(
&p,
scope,
invocation,
source::Event::Edit(token, source::Edit::Use),
)
.unwrap()
.unwrap();
let preview = state.preview(&p, Some(new)).unwrap().unwrap();
assert_eq!(preview.elements[&new].parent, Some(parent));
assert_eq!(preview.elements[&new].kind(), Kind::Activity);
assert_eq!(p.elements.len() + 1, preview.elements.len());
let raw = serde_json::to_value(&state).unwrap();
p.elements.get_mut(&parent).unwrap().body = ElementBody::new(Kind::Entity, Layer::Oa).unwrap();
assert!(state.preview(&p, Some(new)).is_err());
assert_eq!(serde_json::to_value(&state).unwrap(), raw);
}
#[test]
fn contextual_source_new_callee_lifeline_preserves_caller_bindings_as_an_explicit_warning() {
let (mut p, callee) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let caller = p
.add_element(Kind::Scenario, Layer::Oa, "Caller", None)
.unwrap();
let call = topology::occurrence(
&p,
caller,
"Use callee",
OccurrenceBody::ScenarioUse {
scenario: Some(callee),
bindings: vec![],
},
);
let call_id = call.id;
p = execute_batch(
&p,
&[
Command::CreateElement(call),
Command::SetSteps(caller, vec![call_id]),
],
)
.unwrap();
let mut state = scenario::State::default();
state
.update(&p, scenario::Event::Start(callee, scenario::Tool::Lifeline))
.unwrap();
scenario_source(&mut state, &p, "New represented activity", None);
let result = history(&p, state.prepare_pending(&p).unwrap());
for (id, element) in &p.elements {
assert_eq!(result.elements[id], *element);
}
assert!(
validation::validate(&result)
.iter()
.any(|issue| issue.element == Some(call_id)
&& issue.code == "missing_lifeline_binding"
&& issue.severity == validation::Severity::Warning)
);
assert_eq!(result.relations, p.relations);
assert_eq!(result.diagrams, p.diagrams);
}
#[test]
fn contextual_source_empty_history_reports_no_change_and_success_requires_a_real_history_step() {
use crate::app::{App, Message, Page};
use std::sync::Arc;
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let mut app = App::with_project(p.clone());
app.page = Page::Behavior;
app.selected = owner;
app.behavior.active_owner = Some(owner);
let before = app.document.snapshot();
let bundle = app.document.bundle().clone();
for (message, status) in [
(Message::Undo, "Nothing to undo"),
(Message::Redo, "Nothing to redo"),
] {
let _ = app.update(message);
assert_eq!(app.status, status);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &bundle);
assert_eq!(app.selected, owner);
assert_eq!(app.behavior.active_owner, Some(owner));
}
let _ = app.update(Message::Command(Command::Rename(
owner,
"Accepted renamed scenario".into(),
)));
assert_eq!(app.project().named(owner), "Accepted renamed scenario");
let _ = app.update(Message::Undo);
assert_eq!(app.status, "Model change undone");
assert_eq!(app.project(), &p);
let _ = app.update(Message::Redo);
assert_eq!(app.status, "Model change redone");
assert_eq!(app.project().named(owner), "Accepted renamed scenario");
let _ = app.update(Message::Redo);
assert_eq!(app.status, "Nothing to redo");
}
#[test]
fn contextual_source_original_document_remains_bound_after_back_or_choosing_existing() {
for legacy in [false, true] {
for inactive in [false, true] {
let (mut p, owner) =
scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let existing = p
.add_element(Kind::Activity, Layer::Oa, "Existing activity", None)
.unwrap();
let unrelated = p
.add_element(Kind::Scenario, Layer::Oa, "Another valid scenario", None)
.unwrap();
let choice = || crate::app::Choice {
id: existing,
label: "Existing activity".into(),
};
let mut s = scenario::State::default();
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
.unwrap();
s.update(
&p,
scenario::Event::Name("Retained valid lifeline 雨".into()),
)
.unwrap();
s.update(&p, scenario::Event::Target(choice())).unwrap();
if legacy {
let mut raw = s.recovery_snapshot().unwrap().unwrap();
raw.as_object_mut().unwrap().remove("source_origin");
s = scenario::State::from_recovery(raw).unwrap();
}
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&p,
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let open = s.recovery_snapshot().unwrap().unwrap();
assert_eq!(
open["source_origin"][0],
serde_json::to_value(p.id).unwrap()
);
if inactive {
s.update(
&p,
scenario::Event::Source(child(
&open,
source::Edit::Name("Unused staged activity".into()),
)),
)
.unwrap();
s.update(&p, scenario::Event::Source(child(&open, source::Edit::Use)))
.unwrap();
s.update(&p, scenario::Event::Target(choice())).unwrap();
} else {
s.update(
&p,
scenario::Event::Source(child(&open, source::Edit::Back)),
)
.unwrap();
}
assert!(
s.prepare_pending(&p).is_ok(),
"Control must have a valid existing target"
);
let raw = s.recovery_snapshot().unwrap().unwrap();
let mut other = p.clone();
other.id = Id::new_v4();
assert!(
s.prepare_pending(&other)
.unwrap_err()
.contains("original consumer document")
);
assert_eq!(s.recovery_snapshot().unwrap().unwrap(), raw);
let mut borrowed = raw.clone();
borrowed["source_origin"][1] = serde_json::to_value(&p.elements[&unrelated]).unwrap();
let mut wrong = scenario::State::from_recovery(borrowed.clone()).unwrap();
assert!(
wrong
.prepare_pending(&p)
.unwrap_err()
.contains("original consumer")
);
assert!(
wrong
.update(
&p,
scenario::Event::Source(source::Event::Open(id(&borrowed, "invocation")))
)
.is_err()
);
assert_eq!(wrong.recovery_snapshot().unwrap().unwrap(), borrowed);
let mut malformed = raw.clone();
malformed["source_origin"] = serde_json::Value::Null;
assert!(
scenario::State::from_recovery(malformed)
.unwrap()
.prepare_pending(&p)
.is_err()
);
let mut f = topology::direct_tests::fixture(Layer::Sa);
let unrelated =
f.p.add_element(
Kind::FunctionalChain,
Layer::Sa,
"Another valid chain",
None,
)
.unwrap();
let choice = || crate::app::Choice {
id: f.work[0],
label: "Existing function".into(),
};
let mut s = topology::State::default();
s.update(
&f.p,
topology::Event::Start(f.owner, topology::Tool::Work, None),
)
.unwrap();
s.update(&f.p, topology::Event::Target(choice())).unwrap();
s.update(
&f.p,
topology::Event::Name("Retained valid contribution 雨".into()),
)
.unwrap();
if legacy {
let mut raw = s.recovery_snapshot().unwrap().unwrap();
raw.as_object_mut().unwrap().remove("source_origin");
s = topology::State::from_recovery(raw).unwrap();
}
let raw = s.recovery_snapshot().unwrap().unwrap();
s.update(
&f.p,
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
)
.unwrap();
let open = s.recovery_snapshot().unwrap().unwrap();
assert_eq!(
open["source_origin"][0],
serde_json::to_value(f.p.id).unwrap()
);
if inactive {
s.update(
&f.p,
topology::Event::Source(child(
&open,
source::Edit::Name("Unused staged function".into()),
)),
)
.unwrap();
s.update(
&f.p,
topology::Event::Source(child(&open, source::Edit::Use)),
)
.unwrap();
s.update(&f.p, topology::Event::Target(choice())).unwrap();
} else {
s.update(
&f.p,
topology::Event::Source(child(&open, source::Edit::Back)),
)
.unwrap();
}
assert!(s.prepare_pending(&f.p).is_ok());
let raw = s.recovery_snapshot().unwrap().unwrap();
let mut other = f.p.clone();
other.id = Id::new_v4();
assert!(
s.prepare_pending(&other)
.unwrap_err()
.contains("original consumer document")
);
assert_eq!(s.recovery_snapshot().unwrap().unwrap(), raw);
let mut borrowed = raw.clone();
borrowed["source_origin"][1] = serde_json::to_value(&f.p.elements[&unrelated]).unwrap();
let mut wrong = topology::State::from_recovery(borrowed.clone()).unwrap();
assert!(
wrong
.prepare_pending(&f.p)
.unwrap_err()
.contains("original consumer")
);
assert!(
wrong
.update(
&f.p,
topology::Event::Source(source::Event::Open(id(&borrowed, "invocation")))
)
.is_err()
);
assert_eq!(wrong.recovery_snapshot().unwrap().unwrap(), borrowed);
let mut malformed = raw.clone();
malformed["source_origin"] = serde_json::Value::Null;
assert!(
topology::State::from_recovery(malformed)
.unwrap()
.prepare_pending(&f.p)
.is_err()
);
}
}
}