use super::*; use crate::forms::Draft; use oa_core::*; use serde_json::{Value, json}; const OWNER: Id = Id::from_u128(100); const BASE: u128 = 10_000; const PATH: &str = "/body/Oa/Shared/DataType/definition/Record/supertype"; fn element(p: &mut Project, id: Id, kind: Kind, layer: Layer, name: &str, parent: Option) { let generated = p.add_element(kind, layer, name, parent).unwrap(); let mut element = p.elements.remove(&generated).unwrap(); element.id = id; element.alias = format!("object_{}", id.as_u128()); if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() { *definition = DataTypeDefinition::Record { supertype: None }; } p.elements.insert(id, element); } fn catalog() -> Project { let mut p = Project::empty(); p.id = Id::from_u128(1); p.name = "Reference catalog".into(); element( &mut p, OWNER, Kind::DataType, Layer::Oa, "Edited record", None, ); for n in (0..1_000).rev() { let id = Id::from_u128(BASE + n); element(&mut p, id, Kind::DataType, Layer::Oa, "Controller", None); p.elements.get_mut(&id).unwrap().alias = format!("controller_{n:04}"); } element( &mut p, Id::from_u128(200), Kind::DataType, Layer::Sa, "Controller", None, ); element( &mut p, Id::from_u128(201), Kind::Activity, Layer::Oa, "Controller", None, ); p } fn inspector(p: Project, selected: Id) -> App { let mut app = App::with_project(p); let _ = app.update(Message::Select(selected)); app } fn request(app: &App, host: &Host, path: &str) -> adapter::Open { app.reference_draft(host, path).unwrap().reference_open( app.project(), path, Rectangle { x: 790., y: 640., width: 278., height: 32., }, ) } fn open(app: &mut App, host: &Host, path: &str) { let request = request(app, host, path); let message = match host { Host::Inspector => Message::FieldReferenceOpen(request), Host::Review { address, facet } => { Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row { address: *address, facet: Some(facet.clone()), event: RowEvent::ReferenceOpen(request), })) } }; let _ = app.update(message); assert!( app.reference_authoring.opened.is_some(), "field must open: {}", app.status ); } fn event(app: &App, action: Action) -> Message { let opened = app.reference_authoring.opened.as_ref().unwrap(); opened.host.message(opened.chooser.event(action)) } fn search(app: &mut App, query: &str) { let serial = app .reference_authoring .opened .as_ref() .unwrap() .chooser .query_serial() + 1; let event = event( app, Action::Query { text: query.into(), serial, }, ); let _ = app.update(event); } fn choose(app: &mut App, id: Id) { let event = event(app, Action::Choose(id)); let _ = app.update(event); } fn raw(app: &App) -> Value { serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap() } fn review(app: &App) -> &semantic_review::State { let Some(workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else { panic!("Review is open") }; state } fn send_review(app: &mut App, event: RowEvent) { let addressed = review(app).address_event(event); let _ = app.update(Message::Workflow(workflow::Event::semantic(addressed))); } fn review_host(app: &App) -> Host { let semantic_review::Event::Row { address, facet: Some(facet), .. } = review(app).address_event(RowEvent::Preview) else { panic!("Active facet") }; Host::Review { address, facet } } fn trace(p: &mut Project, id: Id, oa: Id, sa: Id) { p.traces.insert( id, Trace { id, alias: format!("trace_{}", id.as_u128()), name: "Rationale".into(), description: String::new(), oa, sa, kind: TraceKind::Manual, baseline: None, }, ); } #[test] fn inspector_reference_browse_is_read_only_then_one_scoped_value_applies_with_exact_undo_redo() { let p = catalog(); let mut app = inspector(p.clone(), OWNER); let before = app.document.bundle().clone(); let revision = app.document.revision(); // Malformed escaped text is unrelated to the selected reference. Browsing // and the scoped write must keep its exact spelling and inactive payload. app.draft.as_mut().unwrap().set_exact(true).unwrap(); let _ = app.update(Message::Field("/name".into(), "\"unfinished".into())); let raw_before = raw(&app); open(&mut app, &Host::Inspector, PATH); assert_eq!( app.reference_authoring .opened .as_ref() .unwrap() .chooser .count(), 1_000 ); search(&mut app, "controller @controller_0720 OA"); let picker = &app.reference_authoring.opened.as_ref().unwrap().chooser; assert_eq!(picker.count(), 1); assert_eq!(picker.highlighted().unwrap().id, Id::from_u128(BASE + 720)); assert_eq!(raw(&app), raw_before); assert_eq!(app.document.bundle(), &before); assert_eq!(app.document.revision(), revision); assert!(!app.document.can_undo()); // Captured search shortcuts cannot accidentally save/apply or undo a model. let _ = app.update(Message::Save(false)); let _ = app.update(Message::ShortcutUndo(false)); assert_eq!(raw(&app), raw_before); assert_eq!(app.document.bundle(), &before); choose(&mut app, Id::from_u128(BASE + 720)); let mut expected_raw = raw_before; *expected_raw["value"].pointer_mut(PATH).unwrap() = json!(Id::from_u128(BASE + 720)); expected_raw["inputs"][PATH] = json!(Id::from_u128(BASE + 720).to_string()); assert_eq!(raw(&app), expected_raw); assert_eq!(app.document.bundle(), &before); assert!(!app.document.can_undo()); let _ = app.update(Message::Apply); assert_eq!( app.document.bundle(), &before, "Unrelated invalid text remains an Apply error" ); let _ = app.update(Message::Field("/name".into(), "\"Edited record\"".into())); let _ = app.update(Message::Apply); let mut expected = p; if let Some(SharedBody::DataType { definition, .. }) = expected.elements.get_mut(&OWNER).unwrap().shared_mut() { *definition = DataTypeDefinition::Record { supertype: Some(Id::from_u128(BASE + 720)), }; } assert_eq!(app.project(), &expected); let accepted = app.document.bundle().clone(); assert_ne!(accepted, before); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &accepted); assert_eq!(app.project(), &expected); } #[test] fn inspector_reference_retains_invalid_missing_and_incompatible_current_until_explicit_clear() { for (raw_id, expected) in [ ("-".into(), "invalid"), (Id::from_u128(999).to_string(), "missing"), (Id::from_u128(200).to_string(), "incompatible"), ] { let mut app = inspector(catalog(), OWNER); app.draft .as_mut() .unwrap() .shape_scoped(PATH, json!(raw_id)) .unwrap(); let original = raw(&app); let accepted = app.document.bundle().clone(); open(&mut app, &Host::Inspector, PATH); let current = &app .reference_authoring .opened .as_ref() .unwrap() .chooser .current; assert!(matches!( (expected, current), ("invalid", chooser::Current::InvalidRaw(_)) | ("missing", chooser::Current::Missing(_)) | ("incompatible", chooser::Current::Incompatible(_)) )); let _ = app.update(Message::Escape); assert_eq!(raw(&app), original); open(&mut app, &Host::Inspector, PATH); let clear = event(&app, Action::Clear); let _ = app.update(clear); assert!( app.draft .as_ref() .unwrap() .value .pointer(PATH) .unwrap() .is_null() ); assert_eq!(app.document.bundle(), &accepted); assert!(!app.document.can_undo()); } } #[test] fn chooser_rejects_old_results_after_context_recovery_reload_or_modal_and_keeps_history_text_out() { for change in 0..7 { let mut app = inspector(catalog(), OWNER); open(&mut app, &Host::Inspector, PATH); let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 1))); match change { 0 => { app.draft .as_mut() .unwrap() .shape_scoped(PATH, json!(Id::from_u128(BASE + 2))) .unwrap(); } 1 => { let saved = app.draft.as_ref().unwrap().recovery_snapshot(); app.draft = Some(Draft::from_recovery(app.project(), saved).unwrap()); } 2 => { app.canvas_document = Id::new_v4(); } 3 => { app.hosted.visible = true; } 4 => { app.draft .as_mut() .unwrap() .set_context(Some(json!({"different":"scope"}))); } 5 => { let _ = app.update(Message::Command(Command::Rename( Id::from_u128(BASE + 3), "Changed model".into(), ))); } _ => { app.source_workspace.edit( "unfinished source".into(), iced::widget::text_editor::Cursor { position: iced::widget::text_editor::Position { line: 0, column: 0 }, selection: None, }, ); } } let expected = raw(&app); let source = app.document.bundle().clone(); let _ = app.update(delayed); assert!(app.reference_authoring.opened.is_none()); assert_eq!(raw(&app), expected); assert_eq!(app.document.bundle(), &source); } let mut p = catalog(); trace(&mut p, Id::from_u128(300), OWNER, Id::from_u128(200)); let app = inspector(p, Id::from_u128(300)); let draft = app.draft.as_ref().unwrap(); let request = draft.reference_open( app.project(), "/baseline/semantic/facets/0/references/0/target", Rectangle::default(), ); assert!(draft.reference_catalog(app.project(), &request, 0).is_err()); } #[test] fn reference_array_row_reorder_and_constructor_switch_invalidate_exact_old_field() { let mut p = Project::empty(); for n in 0..3 { element( &mut p, Id::from_u128(BASE + n), Kind::ExchangeItem, Layer::Oa, "Signal item", None, ); } element(&mut p, OWNER, Kind::Event, Layer::Oa, "Signal", None); *p.elements.get_mut(&OWNER).unwrap().shared_mut().unwrap() = SharedBody::Event { body: EventBody::Signal { items: vec![Id::from_u128(BASE), Id::from_u128(BASE + 1)], }, }; let mut app = inspector(p, OWNER); let list = "/body/Oa/Shared/Event/body/Signal/items"; let path = format!("{list}/0"); open(&mut app, &Host::Inspector, &path); let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 2))); let _ = app.update(Message::FieldArray(list.into(), Some(0), 1)); let expected = raw(&app); assert_eq!( app.draft.as_ref().unwrap().value.pointer(list), Some(&json!([Id::from_u128(BASE + 1), Id::from_u128(BASE)])) ); let _ = app.update(delayed); assert_eq!(raw(&app), expected); open(&mut app, &Host::Inspector, &path); let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 2))); let _ = app.update(Message::FieldVariant( "/body/Oa/Shared/Event/body".into(), json!({"Change":{"expression":"unfinished"}}), )); let expected = raw(&app); let _ = app.update(delayed); assert_eq!(raw(&app), expected); assert!(!app.document.can_undo()); } #[test] fn review_reference_facets_groups_and_attachment_hosts_browse_without_replanning_and_reject_old_facet() { for mode in 0..3 { let mut p = Project::empty(); p.id = Id::from_u128(1); for (id, layer, parent) in [ (100, Layer::Oa, None), (101, Layer::Sa, None), (102, Layer::Sa, None), (103, Layer::Oa, Some(Id::from_u128(100))), (104, Layer::Sa, Some(Id::from_u128(101))), ] { element( &mut p, Id::from_u128(id), if parent.is_some() { Kind::DataField } else { Kind::DataType }, layer, "Record", parent, ); } trace( &mut p, Id::from_u128(300), Id::from_u128(100), Id::from_u128(101), ); let facet = match mode { 0 => FacetSelector::Data(DataFacet::RecordSupertype), 1 => FacetSelector::Data(DataFacet::Constructor), _ => FacetSelector::Child(KeyPair { oa: Some(Id::from_u128(103)), sa: Some(Id::from_u128(104)), }), }; let path = if mode == 2 { "/parent" } else { "/body/Sa/Shared/DataType/definition/Record/supertype" }; let mut app = App::with_project(p.clone()); let _ = app.update(Message::StartSemanticReview(Id::from_u128(100))); if mode == 2 { // A paired attachment is a real catalog facet only after explicit // correspondence; selecting an unstaged synthetic pair is rejected. for (id, oa) in [(103, true), (104, false)] { let choice = crate::app::Choice { id: Id::from_u128(id), label: format!("Record ยท @object_{id}"), }; send_review( &mut app, if oa { RowEvent::ReferenceOa(choice) } else { RowEvent::ReferenceSa(choice) }, ); } send_review(&mut app, RowEvent::AddReference); } send_review(&mut app, RowEvent::Select(facet.clone())); send_review( &mut app, RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)), ); let host = review_host(&app); let record = if mode == 2 { Id::from_u128(104) } else { Id::from_u128(101) }; assert_eq!( app.reference_draft(&host, path) .unwrap_or_else(|| { panic!( "Reference fixture mode={mode}, facet={facet:?}, selected={:?}, decisions={:?}, error={:?}", review(&app).selected, review(&app).decisions, review(&app).error ) }) .original, record ); let before = review(&app).recovery_snapshot().unwrap(); let plan = serde_json::to_value(&review(&app).plan).unwrap(); let proposed = review(&app).proposed.clone(); let source = app.document.bundle().clone(); open(&mut app, &host, path); search(&mut app, "@object_102 SA"); assert_eq!( app.reference_authoring .opened .as_ref() .unwrap() .chooser .count(), 1 ); assert_eq!(review(&app).recovery_snapshot().unwrap(), before); assert_eq!(serde_json::to_value(&review(&app).plan).unwrap(), plan); assert_eq!(review(&app).proposed, proposed); assert_eq!(app.document.bundle(), &source); let old = event(&app, Action::Choose(Id::from_u128(102))); send_review( &mut app, RowEvent::Select(FacetSelector::Header(HeaderFacet::Name)), ); let switched = review(&app).recovery_snapshot().unwrap(); let _ = app.update(old); assert_eq!(review(&app).recovery_snapshot().unwrap(), switched); send_review(&mut app, RowEvent::Select(facet)); open(&mut app, &host, path); choose(&mut app, Id::from_u128(102)); let draft = app.reference_draft(&host, path).unwrap(); let mut expected_record = serde_json::to_value(&p.elements[&record]).unwrap(); *expected_record.pointer_mut(path).unwrap() = json!(Id::from_u128(102)); assert_eq!(draft.materialize().unwrap(), expected_record); assert_eq!(app.document.bundle(), &source); assert!( !app.document.can_undo(), "A choice only stages the active review candidate" ); } } mod mounted; #[test] fn review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_focus() { let mut p = Project::empty(); for (id, layer) in [ (100, Layer::Oa), (101, Layer::Sa), (102, Layer::Sa), (110, Layer::Oa), (111, Layer::Sa), ] { element( &mut p, Id::from_u128(id), Kind::DataType, layer, "Record", None, ); } trace( &mut p, Id::from_u128(300), Id::from_u128(100), Id::from_u128(101), ); trace( &mut p, Id::from_u128(310), Id::from_u128(110), Id::from_u128(111), ); let mut app = App::with_project(p); let _ = app.update(Message::StartSemanticReview(Id::from_u128(300))); let facet = FacetSelector::Data(DataFacet::RecordSupertype); send_review(&mut app, RowEvent::Select(facet.clone())); send_review( &mut app, RowEvent::Decide(facet, Some(SemanticDecision::Candidate)), ); let host = review_host(&app); let path = "/body/Sa/Shared/DataType/definition/Record/supertype"; let old_open = request(&app, &host, path); open(&mut app, &host, path); let old_choice = event(&app, Action::Choose(Id::from_u128(102))); for event in [ semantic_review::Event::ChooseTrace(crate::app::Choice { id: Id::from_u128(310), label: "Second exact trace".into(), }), semantic_review::Event::OpenTrace, ] { let _ = app.update(Message::Workflow(workflow::Event::semantic(event))); } assert_eq!(review(&app).target(app.project()), Some(Id::from_u128(111))); let expected = review(&app).recovery_snapshot().unwrap(); let source = app.document.bundle().clone(); let _ = app.update(old_choice); let Host::Review { address, facet } = host else { unreachable!() }; let _ = app.update(Message::Workflow(workflow::Event::semantic( semantic_review::Event::Row { address, facet: Some(facet), event: RowEvent::ReferenceOpen(old_open), }, ))); assert!(app.reference_authoring.opened.is_none()); assert_eq!(review(&app).recovery_snapshot().unwrap(), expected); assert_eq!(app.document.bundle(), &source); assert!(!app.document.can_undo()); }