use super::*; use crate::document::Document; use oa_core::source_project::ProjectLimits; fn start(p: &Project, layer: Layer, family: Family, name: &str) -> State { let mut state = State::default(); assert!(state.update(p, Event::Open(layer)).unwrap().is_none()); state.update(p, Event::Choose(family)).unwrap(); state.update(p, Event::Name(name.into())).unwrap(); state } #[test] fn mounted_creation_keeps_actions_visible_and_renders_only_six_owner_candidates() { use iced::advanced::{Layout, layout, widget}; use iced::{Rectangle, Size, Vector}; struct Geometry { candidates: Vec, rendered: Vec, 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-creation-actions")) { self.actions = Some(bounds); } if let Some(id) = id.filter(|id| self.candidates.contains(id)) { self.rendered.push(id.clone()); } } fn scrollable( &mut self, id: Option<&widget::Id>, bounds: Rectangle, _: Rectangle, _: Vector, _: &mut dyn widget::operation::Scrollable, ) { if id == Some(&widget::Id::new("behavior-creation-fields")) { self.fields = Some(bounds); } } } let mut p = Project::empty(); for index in 0..105 { p.add_element( Kind::Entity, Layer::Oa, &format!( "{index:03} Long engineering participant name {}", "雨 extended ".repeat(20) ), None, ) .unwrap(); } let candidates: Vec<_> = p .elements .keys() .map(|id| widget::Id::from(format!("behavior-creation-candidate-{id}"))) .collect(); for family in [Family::Sequence, Family::Scenario, Family::Machine] { let state = start(&p, Layer::Oa, family, "Very long exact behavior name 雨"); for size in [ Size::new(500., 570.), Size::new(1050., 570.), Size::new(500., 320.), ] { let mut view: UiElement<'_, Event, iced::Theme, ()> = state.view_with(&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 { candidates: candidates.clone(), rendered: Vec::new(), 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.); assert!( actions.x + actions.width <= size.width && actions.y + actions.height <= size.height ); assert!(fields.height >= 100. && fields.y + fields.height <= actions.y); assert_eq!( geometry.rendered.len(), if family == Family::Machine { 6 } else { 0 } ); } } } fn roundtrip(p: Project, state: &mut State) -> Project { let command = state.update(&p, Event::Submit).unwrap().unwrap(); assert_eq!( command, state.prepare_pending(&p).unwrap(), "preparation must retain reserved IDs" ); let api: Command = serde_json::from_value(serde_json::to_value(&command).unwrap()).unwrap(); assert_eq!(api, command); let expected = execute_batch(&p, &[api]).unwrap(); let target = state.created().unwrap(); let mut document = Document::from_project(p.clone(), ProjectLimits::default()).unwrap(); let before_source = document.bundle().clone(); document.execute(command).unwrap(); assert_eq!(document.project(), &expected); assert!( state.has_pending(), "only successful acknowledgement clears raw fields" ); state.acknowledge_commit(); assert!(!state.has_pending()); assert!(document.project().elements.contains_key(&target.behavior)); let after_source = document.bundle().clone(); assert!(document.undo()); assert_eq!(document.project(), &p); assert_eq!(document.bundle(), &before_source); assert!(!document.can_undo(), "one creation must be one Undo entry"); assert!(document.redo()); assert_eq!(document.project(), &expected); assert_eq!(document.bundle(), &after_source); let reopened = Document::from_bundle(after_source, ProjectLimits::default()).unwrap(); assert_eq!(reopened.project(), &expected); expected } #[test] fn oa_process_and_sa_chain_keep_exact_names_root_ownership_and_one_history_entry() { for layer in [Layer::Oa, Layer::Sa] { let p = Project::empty(); let name = " Supply \"water\" — режим "; let mut state = start(&p, layer, Family::Sequence, name); let target = state.created().unwrap(); let result = roundtrip(p, &mut state); let element = &result.elements[&target.behavior]; assert_eq!(element.name, name); assert_eq!(element.layer(), layer); assert_eq!( element.kind(), if layer == Layer::Oa { Kind::Process } else { Kind::FunctionalChain } ); assert_eq!(element.parent, None); assert!(element.steps().unwrap().is_empty()); assert!(result.diagrams.is_empty()); assert!(result.relations.is_empty()); } } #[test] fn all_four_viewpoints_are_explicit_and_survive_source_history_reopen() { for layer in [Layer::Oa, Layer::Sa] { for viewpoint in viewpoints(layer) { let mut p = Project::empty(); let context = p .add_element( if layer == Layer::Oa { Kind::Capability } else { Kind::SystemCapability }, layer, "Water supply", None, ) .unwrap(); let mut state = start(&p, layer, Family::Scenario, "Nominal supply"); assert!( state .prepare_pending(&p) .unwrap_err() .contains("explicitly") ); assert!( state .update(&p, Event::Viewpoint(ScenarioViewpoint::MixedLegacy)) .is_err() ); let wrong = viewpoints(if layer == Layer::Oa { Layer::Sa } else { Layer::Oa })[0]; assert!(state.update(&p, Event::Viewpoint(wrong)).is_err()); state.update(&p, Event::Viewpoint(viewpoint)).unwrap(); state.update(&p, Event::Context(Some(context))).unwrap(); let id = state.created().unwrap().behavior; let result = roundtrip(p, &mut state); assert_eq!( result.elements[&id].parent, None, "context is not containment" ); assert!( matches!(result.elements[&id].shared(), Some(SharedBody::Scenario { viewpoint: actual, context: Some(actual_context), steps, .. }) if *actual == viewpoint && *actual_context == context && steps.is_empty()) ); } } } #[test] fn machine_requires_valid_owner_and_first_view_adds_no_region_or_state() { for layer in [Layer::Oa, Layer::Sa] { for first_view in [false, true] { let mut p = Project::empty(); let owner = p .add_element( if layer == Layer::Oa { Kind::Entity } else { Kind::System }, layer, "Pump", None, ) .unwrap(); let wrong_kind = p .add_element( if layer == Layer::Oa { Kind::Activity } else { Kind::Function }, layer, "Pump water", None, ) .unwrap(); let opposite = if layer == Layer::Oa { Layer::Sa } else { Layer::Oa }; let wrong_layer = p .add_element( if opposite == Layer::Oa { Kind::Entity } else { Kind::System }, opposite, "Other layer pump", None, ) .unwrap(); let mut state = start(&p, layer, Family::Machine, "Pump modes"); assert!( state .prepare_pending(&p) .unwrap_err() .contains("participant") ); assert!(state.update(&p, Event::Owner(wrong_kind)).is_err()); assert!(state.update(&p, Event::Owner(wrong_layer)).is_err()); state.update(&p, Event::Owner(owner)).unwrap(); state.update(&p, Event::FirstView(first_view)).unwrap(); let target = state.created().unwrap(); let expected_count = p.elements.len() + 1; let result = roundtrip(p, &mut state); assert_eq!(result.elements.len(), expected_count); assert_eq!(result.elements[&target.behavior].parent, Some(owner)); assert_eq!(result.elements[&target.behavior].kind(), Kind::StateMachine); assert_eq!(result.diagrams.len(), usize::from(first_view)); if let Some(view) = target.diagram { let diagram = &result.diagrams[&view]; 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 cancel_and_illegal_family_layer_switches_never_create_model_objects() { let p = Project::empty(); let mut state = start(&p, Layer::Oa, Family::Scenario, "Raw name"); state .update( &p, Event::Viewpoint(ScenarioViewpoint::ParticipantInteraction), ) .unwrap(); let snapshot = state.recovery_snapshot().unwrap(); assert!(state.update(&p, Event::Open(Layer::Sa)).is_err()); assert!(state.update(&p, Event::Choose(Family::Sequence)).is_err()); assert_eq!(state.recovery_snapshot().unwrap(), snapshot); assert!(state.update(&p, Event::Cancel).unwrap().is_none()); assert!(!state.has_pending()); assert!(state.created().is_none()); assert!(state.prepare_pending(&p).is_err()); assert!(p.elements.is_empty()); assert!(p.diagrams.is_empty()); } #[test] fn recovery_preserves_exact_raw_inputs_reserved_ids_and_revalidates_stale_owner() { let mut p = Project::empty(); let owner = p .add_element(Kind::Entity, Layer::Oa, "Plant", None) .unwrap(); let mut state = start(&p, Layer::Oa, Family::Machine, " \nRaw rejected name\0 "); state.update(&p, Event::Owner(owner)).unwrap(); state.update(&p, Event::FirstView(true)).unwrap(); state .update(&p, Event::Search(Picker::Owner, " Plant ".into())) .unwrap(); let raw = state.recovery_snapshot().unwrap().unwrap(); let mut restored = State::from_recovery(raw.clone()).unwrap(); assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw.clone())); assert_eq!(restored.created(), state.created()); assert_eq!(restored.pending_layer(), Some(Layer::Oa)); assert!(restored.prepare_pending(&p).is_err()); restored .update(&p, Event::Name("Plant modes".into())) .unwrap(); let command = restored.prepare_pending(&p).unwrap(); let again = State::from_recovery(restored.recovery_snapshot().unwrap().unwrap()).unwrap(); assert_eq!(again.prepare_pending(&p).unwrap(), command); p.elements.remove(&owner); let before = restored.recovery_snapshot().unwrap(); assert!( restored .prepare_pending(&p) .unwrap_err() .contains("participant") ); assert_eq!(restored.recovery_snapshot().unwrap(), before); let different = Project::empty(); assert!( restored .prepare_pending(&different) .unwrap_err() .contains("another document") ); } #[test] fn rejected_source_transaction_keeps_draft_and_every_accepted_source_byte() { let p = Project::empty(); 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 + 32, ..language::ParseLimits::UNLIMITED }, ..ProjectLimits::default() }; let mut doc = Document::from_bundle(initial.bundle().clone(), limits).unwrap(); let mut state = start( &p, Layer::Oa, Family::Sequence, "A long exact name retained after the source budget refuses this new process", ); let raw = state.recovery_snapshot().unwrap(); let command = state.prepare_pending(&p).unwrap(); assert!(doc.execute(command).is_err()); assert_eq!(doc.project(), &p); assert_eq!(doc.bundle(), initial.bundle()); assert!(!doc.can_undo()); assert_eq!(state.recovery_snapshot().unwrap(), raw); state.update(&p, Event::Cancel).unwrap(); } #[test] fn owner_search_is_name_ordered_filtered_and_page_sized_not_a_catalog() { let mut p = Project::empty(); for index in (0..105).rev() { p.add_element(Kind::Entity, Layer::Oa, &format!("Pump {index:03}"), None) .unwrap(); } p.add_element(Kind::System, Layer::Sa, "Pump 999", None) .unwrap(); p.add_element(Kind::Activity, Layer::Oa, "Pump work", None) .unwrap(); let mut state = start(&p, Layer::Oa, Family::Machine, "Pump modes"); let all = choices(&p, Layer::Oa, Picker::Owner, ""); assert_eq!(all.len(), 105); assert_eq!(all[0].name, "Pump 000"); state .update(&p, Event::Page(Picker::Owner, usize::MAX)) .unwrap(); assert!( state.view(&p).is_some(), "stale pages are clamped before slicing" ); state .update(&p, Event::Search(Picker::Owner, "Pump 104".into())) .unwrap(); assert_eq!(state.draft.as_ref().unwrap().browsers[0].page, 0); let found = choices(&p, Layer::Oa, Picker::Owner, "Pump 104"); assert_eq!(found.len(), 1); state.update(&p, Event::Owner(found[0].id)).unwrap(); assert_eq!(state.draft.as_ref().unwrap().owner, Some(found[0].id)); } #[test] fn repeated_owner_names_remain_distinguishable_by_searchable_parent_context_and_alias() { let mut p = Project::empty(); let north = p .add_element(Kind::Entity, Layer::Oa, "North station", None) .unwrap(); let south = p .add_element(Kind::Entity, Layer::Oa, "South station", None) .unwrap(); let first = p .add_element(Kind::Entity, Layer::Oa, "Pump", Some(north)) .unwrap(); let second = p .add_element(Kind::Entity, Layer::Oa, "Pump", Some(south)) .unwrap(); let label = reference_label(&p, second); assert_ne!(reference_label(&p, first), label); assert!(label.contains("South station") && label.contains("Pump")); assert!(label.contains(&p.elements[&second].alias)); let found = choices(&p, Layer::Oa, Picker::Owner, "south station"); assert!(found.iter().any(|e| e.id == second)); assert!(!found.iter().any(|e| e.id == first)); let mut state = start(&p, Layer::Oa, Family::Machine, "Pump states"); state.update(&p, Event::Owner(second)).unwrap(); let result = execute_batch(&p, &[state.prepare_pending(&p).unwrap()]).unwrap(); assert_eq!( result.elements[&state.created().unwrap().behavior].parent, Some(second) ); } #[test] fn corrupted_recovery_and_reserved_identity_collision_refuse_without_repairing_raw_values() { let p = Project::empty(); let state = start(&p, Layer::Sa, Family::Sequence, "Supply"); let raw = state.recovery_snapshot().unwrap().unwrap(); for (field, value) in [ ("version", serde_json::json!(99)), ("unexpected", serde_json::json!(true)), ] { let mut bad = raw.clone(); bad[field] = value; assert!(State::from_recovery(bad).is_err()); } let mut bad = raw; bad["element"] = serde_json::json!(p.id); let restored = State::from_recovery(bad.clone()).unwrap(); assert!( restored .prepare_pending(&p) .unwrap_err() .contains("identity") ); assert_eq!(restored.recovery_snapshot().unwrap(), Some(bad)); } #[test] fn post_commit_selection_is_correlated_with_exact_created_behavior_and_view() { let mut p = Project::empty(); let owner = p .add_element(Kind::Entity, Layer::Oa, "Pump", None) .unwrap(); let mut state = start(&p, Layer::Oa, Family::Machine, "Pump modes"); state.update(&p, Event::Owner(owner)).unwrap(); state.update(&p, Event::FirstView(true)).unwrap(); let command = state.prepare_pending(&p).unwrap(); assert_eq!(state.created_by(&p, &command), state.created()); assert_eq!( state.created_by(&p, &Command::Rename(owner, "Unrelated change".into())), None ); for edit in 0..4 { let mut changed = command.clone(); let Command::Batch(ref mut commands) = changed else { panic!("creation batch"); }; if edit < 3 { let Command::CreateElement(element) = &mut commands[0] else { panic!("new behavior"); }; match edit { 0 => element.id = Id::new_v4(), 1 => element.name = "A different authored draft".into(), _ => element.parent = None, } } else { let Command::CreateDiagram(diagram) = &mut commands[1] else { panic!("new view"); }; diagram.projection = ProjectionKind::StateMachine { machine: owner }; } assert_eq!(state.created_by(&p, &changed), None); } let Command::Batch(commands) = command else { panic!("creation batch"); }; assert_eq!( state.created_by(&p, &commands[0]), None, "an opt-in view must be part of the actual committed batch" ); } #[test] fn optional_context_collapses_without_losing_authored_reference_or_raw_search() { let mut p = Project::empty(); let context = p .add_element(Kind::Capability, Layer::Oa, "Water supply", None) .unwrap(); let mut state = start(&p, Layer::Oa, Family::Scenario, "Supply nominal"); assert!(!state.draft.as_ref().unwrap().context_open); state .update(&p, Event::Viewpoint(ScenarioViewpoint::OperationalActivity)) .unwrap(); state.update(&p, Event::ContextOptions(true)).unwrap(); state .update(&p, Event::Search(Picker::Context, " Water ".into())) .unwrap(); state.update(&p, Event::Context(Some(context))).unwrap(); let command = state.prepare_pending(&p).unwrap(); state.update(&p, Event::ContextOptions(false)).unwrap(); let restored = State::from_recovery(state.recovery_snapshot().unwrap().unwrap()).unwrap(); assert!(!restored.draft.as_ref().unwrap().context_open); assert_eq!( restored.draft.as_ref().unwrap().browsers[1].query, " Water " ); assert_eq!(restored.prepare_pending(&p).unwrap(), command); assert_eq!(restored.created_by(&p, &command), restored.created()); } #[test] fn creation_selection_rejects_changed_scenario_body_and_post_creation_operations() { let mut p = Project::empty(); let capability = p .add_element(Kind::SystemCapability, Layer::Sa, "Supply", None) .unwrap(); let mut state = start(&p, Layer::Sa, Family::Scenario, "Nominal functions"); state .update(&p, Event::Viewpoint(ScenarioViewpoint::Functional)) .unwrap(); let command = state.prepare_pending(&p).unwrap(); assert_eq!(state.created_by(&p, &command), state.created()); for edit in 0..4 { let mut altered = command.clone(); let Command::Batch(commands) = &mut altered else { panic!("creation batch"); }; match edit { 0 => { let Command::CreateElement(element) = &mut commands[0] else { panic!("scenario"); }; let Some(SharedBody::Scenario { viewpoint, .. }) = element.shared_mut() else { panic!("scenario body"); }; *viewpoint = ScenarioViewpoint::BoundaryInterface; } 1 => { let Command::CreateElement(element) = &mut commands[0] else { panic!("scenario"); }; let Some(SharedBody::Scenario { context, .. }) = element.shared_mut() else { panic!("scenario body"); }; *context = Some(capability); } 2 => commands.push(Command::Rename( state.created().unwrap().behavior, "Changed afterward".into(), )), _ => commands.push(Command::Delete(state.created().unwrap().behavior)), } assert!( execute_batch(&p, std::slice::from_ref(&altered)).is_ok(), "the alternate command is valid, but is not this authored creation" ); assert_eq!(state.created_by(&p, &altered), None); } }