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) { 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, fields: Option, } 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() ); } } }