use super::*; use crate::app::Page; use oa_core::{Project, language::Span}; use std::sync::Arc; fn fixture() -> App { let mut app = App::with_project(Project::empty()); app.page = Page::Text; let diagnostics = app.source_diagnostics.clone(); app.publish_source_diagnostics(diagnostics); assert!(app.source_navigation_available()); app } fn send(app: &mut App, event: Event) { // Exercise the real envelope path; worker/lint futures are deliberately not // polled here. The parent owns serialized compile/runtime verification. let _ = app.update(Message::TextWorkspace(event)); } fn toggle(app: &App, stamp: QueryStamp, path: String) -> Event { Event::ToggleFile { stamp, visit: app.text_workspace.drawer_visit(), path, } } fn open(app: &mut App) -> Id { let message = app.text_menu_message(Point::new(10., 20.)); let _ = app.update(message); app.text_workspace.menu().unwrap().token() } fn invoke(app: &mut App, token: Id, command: Command) { send(app, Event::Invoke { token, command }); } fn enabled(app: &App, command: Command) -> bool { app.text_workspace .menu .as_ref() .unwrap() .entries .iter() .find(|entry| entry.command == command) .is_some_and(|entry| entry.action.is_some()) } fn back_entry(app: &App) -> nav::HistoryEntry { nav::HistoryEntry { session: app.source_workspace.session(), path: app.source_workspace.active.clone(), generation: app.source_workspace.files[&app.source_workspace.active].generation(), cursor: app.source_editor.cursor(), } } fn stamp(app: &App, serial: u64) -> QueryStamp { QueryStamp { snapshot: app.source_key(), serial, } } fn location(app: &App, start: usize, end: usize) -> SourceLocation { SourceLocation { path: app.source_workspace.active.clone(), span: Span { start, end }, } } #[test] fn menu_own_open_invalidation_does_not_stale_find_and_invoke_is_once() { let mut app = fixture(); let old_interaction = app.source_symbols.interaction; let token = open(&mut app); assert_ne!(old_interaction, app.source_symbols.interaction); assert_eq!( app.text_workspace.menu.as_ref().unwrap().context, app.text_command_context() ); assert!(enabled(&app, Command::Find)); invoke(&mut app, token, Command::Find); assert!(app.source_navigation.open); assert!(app.text_workspace.menu().is_none()); let instance = app.source_find_instance; invoke(&mut app, token, Command::Find); assert_eq!( app.source_find_instance, instance, "replay must not reopen Find" ); } #[test] fn disabled_entry_cannot_become_a_new_action_under_the_same_menu_token() { let mut app = fixture(); let token = open(&mut app); assert!(!enabled(&app, Command::Back)); app.source_symbols.back.push(back_entry(&app)); assert!( !enabled(&app, Command::Back), "frozen presentation must remain disabled" ); invoke(&mut app, token, Command::Back); assert_eq!( app.source_symbols.back.len(), 1, "a newly enabled action cannot be substituted" ); assert!(app.source_symbols.reveal.is_none()); } #[test] fn enabled_entry_is_readmitted_at_dispatch_and_blocked_by_busy() { let mut app = fixture(); let token = open(&mut app); assert!(enabled(&app, Command::Find)); app.busy = true; assert!(enabled(&app, Command::Find)); invoke(&mut app, token, Command::Find); assert!(!app.source_navigation.open); assert!(app.text_workspace.menu().is_none()); } #[test] fn old_dismiss_and_invoke_do_not_consume_a_newer_menu() { let mut app = fixture(); let old = open(&mut app); send(&mut app, Event::DismissMenu(old)); let new = open(&mut app); send(&mut app, Event::DismissMenu(old)); invoke(&mut app, old, Command::Find); assert_eq!(app.text_workspace.menu().unwrap().token(), new); assert!(!app.source_navigation.open); } #[test] fn menu_source_caret_document_path_and_interaction_changes_refuse_stale_find() { for change in 0..6 { let mut app = fixture(); let token = open(&mut app); match change { 0 => { let value = format!("{}\n// changed", app.source_workspace.text()); app.source_workspace.edit(value, app.source_editor.cursor()); } 1 => { let mut cursor = app.source_editor.cursor(); cursor.position.column += 1; app.source_editor.move_to(cursor); } 2 => app.canvas_document = Id::new_v4(), 3 => app.source_workspace.active = "other.arcadia".into(), 4 => app.source_symbols.interaction = Id::new_v4(), _ => { let mut bundle = app.document.bundle().clone(); bundle .files .get_mut(&bundle.entry) .unwrap() .push_str("\n// accepted revision"); assert!(app.document.apply_bundle(bundle).unwrap()); } } invoke(&mut app, token, Command::Find); assert!(!app.source_navigation.open, "stale context case {change}"); } } #[test] fn source_and_property_drafts_keep_find_and_text_undo_without_model_application() { let mut app = fixture(); let accepted = app.document.bundle().clone(); let revision = app.document.revision(); let pending_name = "Pending property draft"; // Use the actual property input route before source edits coexist. Changing // Draft::value alone is overwritten by its authoritative raw input buffer. let _ = app.update(Message::Field("/name".into(), pending_name.into())); let assert_pending_property = |app: &App| { let draft = app.draft.as_ref().unwrap(); assert!(draft.dirty()); assert_eq!(draft.inputs["/name"].raw, pending_name); assert_eq!(draft.inputs["/name"].editor.text(), pending_name); assert_eq!( draft.materialize().unwrap()["name"].as_str(), Some(pending_name) ); }; assert_pending_property(&app); let initial = app.source_workspace.text().to_owned(); app.source_workspace .edit(format!("{initial}\n// draft"), app.source_editor.cursor()); app.show_source_file(false); assert!(app.source_dirty() && app.draft_dirty()); let token = open(&mut app); assert!(enabled(&app, Command::Find) && enabled(&app, Command::UndoText)); assert!(!enabled(&app, Command::RenameAlias) && !enabled(&app, Command::UndoModel)); invoke(&mut app, token, Command::Find); assert!(app.source_navigation.open); assert_pending_property(&app); let token = open(&mut app); invoke(&mut app, token, Command::UndoText); assert_eq!(app.source_workspace.text(), initial); assert_pending_property(&app); assert_eq!(app.document.revision(), revision); assert_eq!(app.document.bundle(), &accepted); } #[test] fn queued_toolbar_request_cannot_retarget_after_source_change() { let mut app = fixture(); let request = app.text_command_message(Command::Find); app.source_workspace .edit("// changed".into(), app.source_editor.cursor()); let _ = app.update(request); assert!(!app.source_navigation.open); } #[test] fn equal_diagnostic_republication_invalidates_primary_and_related_targets() { let mut app = fixture(); { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems); send(&mut app, captured); }; let primary = app.text_diagnostic_message(location(&app, 0, 1)).unwrap(); let related = app.text_diagnostic_message(location(&app, 1, 2)).unwrap(); let previous = app.text_workspace.diagnostic_publication().unwrap(); app.publish_source_diagnostics(app.source_diagnostics.clone()); let current = app.text_workspace.diagnostic_publication().unwrap(); assert_eq!(previous.source(), current.source()); assert_ne!(previous.generation(), current.generation()); for message in [primary, related] { let interaction = app.source_symbols.interaction; let _ = app.update(message); assert_eq!(app.source_symbols.interaction, interaction); assert!(app.source_symbols.reveal.is_none()); } } #[test] fn current_diagnostic_target_navigates_but_changed_source_or_clear_refuses_it() { let mut app = fixture(); { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems); send(&mut app, captured); }; let message = app.text_diagnostic_message(location(&app, 0, 1)).unwrap(); let _ = app.update(message); assert!(app.source_symbols.reveal.is_some()); app.source_interaction(); let message = app.text_diagnostic_message(location(&app, 0, 1)).unwrap(); app.source_workspace .edit("// newer".into(), app.source_editor.cursor()); let _ = app.update(message); assert!(app.source_symbols.reveal.is_none()); assert!(app.text_diagnostic_message(location(&app, 0, 1)).is_none()); app.publish_source_diagnostics(Vec::new()); let message = app.text_diagnostic_message(location(&app, 0, 1)).unwrap(); app.publish_source_diagnostics(Vec::new()); let _ = app.update(message); assert!(app.source_symbols.reveal.is_none()); } #[test] fn diagnostic_span_must_be_loaded_ordered_and_utf8_exact() { let mut app = fixture(); { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems); send(&mut app, captured); }; app.source_workspace .edit("a雨b".into(), app.source_editor.cursor()); app.show_source_file(false); app.publish_source_diagnostics(Vec::new()); let mut missing = location(&app, 0, 1); missing.path = "missing.arcadia".into(); for target in [ location(&app, 3, 2), location(&app, 0, 6), location(&app, 2, 4), location(&app, 1, 3), missing, ] { let message = app.text_diagnostic_message(target).unwrap(); let _ = app.update(message); assert!(app.source_symbols.reveal.is_none()); } let message = app.text_diagnostic_message(location(&app, 1, 4)).unwrap(); let _ = app.update(message); assert!(app.source_symbols.reveal.is_some()); } #[test] fn reference_groups_and_scroll_survive_tabs_and_reject_old_visits_and_queries() { let mut app = fixture(); let first = stamp(&app, 1); app.text_workspace .begin_references(first, "b/use.arcadia".into()); app.text_workspace .install_reference_files(first, ["a/use.arcadia".into(), "b/use.arcadia".into()]); assert!(app.text_workspace.references().collapsed("a/use.arcadia")); assert!(!app.text_workspace.references().collapsed("b/use.arcadia")); let event = toggle(&app, first, "a/use.arcadia".into()); send(&mut app, event); let old_visit = app.text_workspace.drawer_visit(); let old_scroll_id = app.text_workspace.reference_scroll_id(); let interaction = app.source_symbols.interaction; send( &mut app, Event::ReferenceScrolled { stamp: first, visit: old_visit, y: 123., }, ); assert_eq!( app.source_symbols.interaction, interaction, "scroll is passive" ); { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Status); send(&mut app, captured); }; send( &mut app, Event::ReferenceScrolled { stamp: first, visit: old_visit, y: 0., }, ); { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::References); send(&mut app, captured); }; assert_eq!(app.text_workspace.references().scroll_y(), 123.); assert!(!app.text_workspace.references().collapsed("a/use.arcadia")); send( &mut app, Event::ReferenceScrolled { stamp: first, visit: old_visit, y: 0., }, ); assert_eq!(app.text_workspace.references().scroll_y(), 123.); assert_ne!(app.text_workspace.reference_scroll_id(), old_scroll_id); let second = stamp(&app, 2); app.text_workspace .begin_references(second, "absent.arcadia".into()); app.text_workspace .install_reference_files(first, ["old.arcadia".into()]); app.text_workspace .install_reference_files(second, ["b/use.arcadia".into(), "a/use.arcadia".into()]); assert_eq!(app.text_workspace.references().scroll_y(), 0.); assert!(!app.text_workspace.references().collapsed("a/use.arcadia")); assert!(app.text_workspace.references().collapsed("b/use.arcadia")); let event = toggle(&app, first, "b/use.arcadia".into()); send(&mut app, event); assert!(app.text_workspace.references().collapsed("b/use.arcadia")); } #[test] fn reset_removes_ui_and_publication_authority_and_resize_rejects_nan() { let mut app = fixture(); open(&mut app); app.text_workspace.reset(); assert!(app.text_workspace.menu().is_none()); assert!(app.text_workspace.diagnostic_publication().is_none()); assert!(app.text_workspace.drawer().is_none()); { let captured = Event::ResizeDrawer(app.text_drawer_context(), f32::NAN); send(&mut app, captured); }; assert_eq!(app.text_workspace.drawer_height(), 180.); { let captured = Event::ResizeDrawer(app.text_drawer_context(), f32::MAX); send(&mut app, captured); }; assert_eq!(app.text_workspace.drawer_height(), 1200.); } #[test] fn only_valid_explicit_group_and_resize_intent_cancel_pending_navigation() { let mut app = fixture(); let current = stamp(&app, 1); app.text_workspace .begin_references(current, "uses.arcadia".into()); app.text_workspace .install_reference_files(current, ["uses.arcadia".into()]); let request = nav::Request { job: Id::new_v4(), interaction: app.source_symbols.interaction, raw_interaction: 0, stamp: current, path: app.source_workspace.active.clone(), offset: 0, cursor: app.source_editor.cursor(), action: nav::query::Action::References, }; app.source_symbols.request = Some(request.clone()); app.source_symbols.in_flight = Some(request.job); let interaction = app.source_symbols.interaction; let event = toggle(&app, current, "not-a-group.arcadia".into()); send(&mut app, event); let old = QueryStamp { serial: 0, ..current }; let event = toggle(&app, old, "uses.arcadia".into()); send(&mut app, event); { let captured = Event::ResizeDrawer(app.text_drawer_context(), f32::NAN); send(&mut app, captured); }; { let captured = Event::ResizeDrawer(app.text_drawer_context(), 180.); send(&mut app, captured); }; assert_eq!(app.source_symbols.interaction, interaction); assert!(app.source_symbols.request.is_some()); let event = toggle(&app, current, "uses.arcadia".into()); send(&mut app, event); assert!(app.source_symbols.request.is_none()); assert_ne!(app.source_symbols.interaction, interaction); assert_eq!( app.source_symbols.in_flight, Some(request.job), "intent does not release an occupied worker" ); app.source_symbols.request = Some(request.clone()); { let captured = Event::ResizeDrawer(app.text_drawer_context(), 240.); send(&mut app, captured); }; assert!(app.source_symbols.request.is_none()); app.source_symbols.request = Some(request.clone()); { let captured = Event::HideDrawer(app.text_drawer_context()); send(&mut app, captured); }; assert!(app.source_symbols.request.is_none()); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert_eq!(app.text_workspace.references().stamp(), Some(current)); } #[test] fn stale_retained_group_can_expand_without_navigating_or_applying_drafts() { let mut app = fixture(); let current = stamp(&app, 1); let path = app.source_workspace.active.clone(); app.text_workspace.begin_references(current, path.clone()); app.text_workspace .install_reference_files(current, [path.clone()]); let event = toggle(&app, current, path.clone()); send(&mut app, event); assert!(app.text_workspace.references().collapsed(&path)); let accepted = app.document.snapshot(); let _ = app.update(Message::SourceEdit(widget::text_editor::Action::Edit( widget::text_editor::Edit::Insert(' '), ))); assert_ne!(current.snapshot, app.source_key()); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); let event = toggle(&app, current, path.clone()); send(&mut app, event); assert!(!app.text_workspace.references().collapsed(&path)); assert_eq!(app.source_workspace.active, path); assert_eq!(app.source_workspace.bundle(), raw); assert_eq!(app.source_editor.cursor(), cursor); assert!(app.source_symbols.reveal.is_none() && app.source_symbols.back.is_empty()); assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); } #[test] fn document_reset_preserves_popup_token_sequence_and_drops_old_menu_authority() { let mut app = fixture(); let old = open(&mut app); let sequence = app.text_workspace_menu().unwrap().token; app.reset(Project::empty(), None); assert!(app.text_workspace.menu().is_none()); app.page = Page::Text; let fresh = open(&mut app); assert_ne!(old, fresh); assert!(app.text_workspace_menu().unwrap().token > sequence); invoke(&mut app, old, Command::Find); assert!(!app.source_navigation.open); assert_eq!(app.text_workspace.menu().unwrap().token(), fresh); } #[test] fn retained_primary_and_related_targets_cannot_replay_after_hide_or_problems_references_problems() { for hide in [false, true] { let mut app = fixture(); { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems); send(&mut app, captured); }; let old_visit = app.text_workspace.drawer_visit(); let publication = app.text_workspace.diagnostic_publication(); let targets = [location(&app, 0, 1), location(&app, 1, 2)]; let old = targets .iter() .cloned() .map(|target| app.text_diagnostic_message(target).unwrap()) .collect::>(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let history = ( app.document.revision(), app.document.can_undo(), app.document.can_redo(), app.document.dirty(), ); let event = if hide { Event::HideDrawer(app.text_drawer_context()) } else { Event::ShowDrawer(app.text_drawer_context(), DrawerTab::References) }; send(&mut app, event); for reopened in [false, true] { if reopened { { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems); send(&mut app, captured); }; } else { assert!(app.text_diagnostic_message(targets[0].clone()).is_none()); } assert_ne!(app.text_workspace.drawer_visit(), old_visit); assert_eq!( app.text_workspace.diagnostic_publication(), publication, "same list and source isolate the visit guard from publication freshness" ); let context = app.text_command_context(); let drawer = app.text_workspace.drawer(); let visit = app.text_workspace.drawer_visit(); for message in old.iter().cloned() { let _ = app.update(message); assert_eq!( app.text_command_context(), context, "stale diagnostic must fail before interaction invalidation" ); assert_eq!(app.text_workspace.drawer(), drawer); assert_eq!(app.text_workspace.drawer_visit(), visit); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert!(app.source_symbols.back.is_empty()); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!( ( app.document.revision(), app.document.can_undo(), app.document.can_redo(), app.document.dirty() ), history ); } } for target in targets { let fresh = app.text_diagnostic_message(target).unwrap(); let _ = app.update(fresh); assert!( app.source_symbols.reveal.is_some(), "fresh primary and related targets remain usable" ); app.source_interaction(); } assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } } #[test] fn queued_group_toggle_cannot_mutate_a_reopened_or_rotated_visit() { for replacement in 0..3 { let mut app = fixture(); let current = stamp(&app, 1); app.text_workspace .begin_references(current, "a.arcadia".into()); app.text_workspace .install_reference_files(current, ["a.arcadia".into(), "b.arcadia".into()]); let old = toggle(&app, current, "a.arcadia".into()); let old_visit = app.text_workspace.drawer_visit(); match replacement { 0 => { { let captured = Event::HideDrawer(app.text_drawer_context()); send(&mut app, captured); }; { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::References); send(&mut app, captured); }; } 1 => { { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems); send(&mut app, captured); }; { let captured = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::References); send(&mut app, captured); }; } _ => { let event = toggle(&app, current, "b.arcadia".into()); send(&mut app, event); } } let visit = app.text_workspace.drawer_visit(); assert_ne!(visit, old_visit); send( &mut app, Event::ReferenceScrolled { stamp: current, visit, y: 84., }, ); let scroll_id = app.text_workspace.reference_scroll_id(); let request = nav::Request { job: Id::new_v4(), interaction: app.source_symbols.interaction, raw_interaction: 0, stamp: current, path: app.source_workspace.active.clone(), offset: 0, cursor: app.source_editor.cursor(), action: nav::query::Action::References, }; app.source_symbols.request = Some(request.clone()); app.source_symbols.in_flight = Some(request.job); let groups = ["a.arcadia", "b.arcadia"].map(|path| app.text_workspace.references().collapsed(path)); let context = app.text_command_context(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); for _ in 0..2 { send(&mut app, old.clone()); assert_eq!(app.text_workspace.drawer_visit(), visit); assert_eq!(app.text_workspace.references().scroll_y(), 84.); assert_eq!(app.text_workspace.reference_scroll_id(), scroll_id); assert_eq!(app.text_workspace.references().stamp(), Some(current)); assert_eq!( ["a.arcadia", "b.arcadia"] .map(|path| app.text_workspace.references().collapsed(path)), groups, ); assert_eq!(app.text_command_context(), context); assert_eq!(app.source_symbols.request.as_ref(), Some(&request)); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); } let fresh = toggle(&app, current, "a.arcadia".into()); send(&mut app, fresh); assert_eq!( app.text_workspace.references().collapsed("a.arcadia"), !groups[0] ); assert_ne!(app.text_workspace.drawer_visit(), visit); assert_ne!(app.source_symbols.interaction, context.interaction); assert!(app.source_symbols.request.is_none()); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert!(app.source_symbols.back.is_empty()); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.document.can_undo() && !app.document.can_redo()); } } #[test] fn annotation_snapshot_requires_exact_current_bytes_and_publication() { let mut app = fixture(); let original_model = app.document.snapshot(); let original_source = app.source_snapshot.clone(); let publication = app.source_annotation_snapshot().unwrap().publication; let initial = app.source_annotation_snapshot().unwrap(); assert!(Arc::ptr_eq(&initial.source, &original_source)); assert_eq!(initial.path, app.source_workspace.active); assert!(std::ptr::eq( initial.diagnostics, app.source_diagnostics.as_slice() )); // Republish identical diagnostics: old cached geometry must get a new identity. app.publish_source_diagnostics(app.source_diagnostics.clone()); assert_ne!( app.source_annotation_snapshot().unwrap().publication, publication ); app.source_workspace .edit("// newer draft\n".into(), app.source_editor.cursor()); assert!( app.source_annotation_snapshot().is_none(), "old publication after a real draft edit" ); // A current source key alone must not bind new ranges to the old displayed text. app.publish_source_diagnostics(Vec::new()); assert!( app.source_annotation_snapshot().is_none(), "displayed snapshot still contains old bytes" ); app.show_source_file(false); let current = app.source_annotation_snapshot().unwrap(); assert_eq!(current.source.as_ref(), "// newer draft\n"); assert!(current.diagnostics.is_empty()); assert!(Arc::ptr_eq(&original_model, &app.document.snapshot())); app.text_workspace.reset(); assert!(app.source_annotation_snapshot().is_none()); } #[test] fn annotation_snapshot_tracks_real_file_round_trips_without_changing_model() { let mut project = Project::empty(); let element = project .add_element( oa_core::Kind::Activity, oa_core::Layer::Oa, "Assess incident", None, ) .unwrap(); let mut app = App::with_project(project); app.page = Page::Text; app.publish_source_diagnostics(app.source_diagnostics.clone()); let model = app.document.snapshot(); let entry = app.source_workspace.entry.clone(); let fragment = app.document.compiled().index.declarations[&element] .path .clone(); assert_ne!(entry, fragment); for path in [&entry, &fragment, &entry] { let _ = app.update(Message::SourceFile(path.clone())); let current = app.source_annotation_snapshot().unwrap(); assert_eq!(current.path, path); assert_eq!( current.source.as_ref(), app.source_workspace.files[path].text ); assert!(Arc::ptr_eq(¤t.source, &app.source_snapshot)); assert!(Arc::ptr_eq(&model, &app.document.snapshot())); } } #[test] fn drawer_document_session_and_visit_guards_are_independently_required() { for component in 0..3 { for action in 0..3 { let mut app = fixture(); app.text_workspace.show_drawer(Some(DrawerTab::Status)); let mut captured = app.text_drawer_context(); match component { 0 => captured.document = Id::new_v4(), 1 => captured.session = Id::new_v4(), _ => captured.visit = Id::new_v4(), } app.queue_source_reveal(app.source_editor.cursor(), false); let reveal = app.source_reveal_ticket(); assert!(reveal.is_some()); let interaction = app.source_symbols.interaction; let cursor = app.source_editor.cursor(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let current = app.text_drawer_context(); let height = app.text_workspace.drawer_height(); let event = match action { 0 => Event::ShowDrawer(captured, DrawerTab::Problems), 1 => Event::HideDrawer(captured), _ => Event::ResizeDrawer(captured, height + 60.), }; send(&mut app, event); assert_eq!(app.text_drawer_context(), current); assert_eq!(app.text_workspace.drawer(), Some(DrawerTab::Status)); assert_eq!(app.text_workspace.drawer_height(), height); assert_eq!(app.source_reveal_ticket(), reveal); assert_eq!(app.source_symbols.interaction, interaction); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.document.can_undo() && !app.document.can_redo()); } } } #[cfg(debug_assertions)] #[test] fn actual_chooser_blocks_diagnostic_actions_and_mounted_hover_until_closed() { use oa_core::{language::Diagnostic, source_project::SourceDiagnostic, validation::Severity}; for republish in [false, true] { let mut app = fixture(); assert!(app.source_workspace.text().starts_with("arcadia")); app.publish_source_diagnostics(vec![SourceDiagnostic { path: app.source_workspace.active.clone(), diagnostic: Diagnostic { severity: Severity::Warning, code: "union".into(), message: "Union diagnostic details".into(), object_id: None, field: None, line: 1, column: 1, span: Span { start: 0, end: 7 }, }, related: Vec::new(), }]); let show = Event::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems); send(&mut app, show); let old_target = app.text_diagnostic_message(location(&app, 0, 7)).unwrap(); let mut mounted = crate::source_annotations::AppMounted::new( app.source_annotation_snapshot().unwrap(), app.source_navigation_available(), app.source_symbols.raw_interaction.clone(), ); mounted.assert_hover(true); let open = app.diagram_usage_message(app.project().id); let _ = app.update(open); let token = app .diagram_usages .as_ref() .expect("actual chooser opened") .token; assert!(!app.source_navigation_available()); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); let interaction = app.source_symbols.interaction; let reveal = app.source_reveal_ticket(); if republish { app.publish_source_diagnostics(app.source_diagnostics.clone()); } mounted.refresh( app.source_annotation_snapshot().unwrap(), app.source_navigation_available(), ); mounted.assert_hover(false); let _ = app.update(old_target.clone()); assert_eq!(app.source_symbols.interaction, interaction); assert_eq!(app.source_reveal_ticket(), reveal); assert_eq!(app.diagram_usages.as_ref().unwrap().token, token); assert_eq!(app.diagram_lookup_in_flight, Some(token)); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); let _ = app.update(Message::DiagramUsage(crate::diagram_usage::Event::Close( token, ))); assert!(app.source_navigation_available()); mounted.refresh( app.source_annotation_snapshot().unwrap(), app.source_navigation_available(), ); mounted.assert_hover(true); if republish { let interaction = app.source_symbols.interaction; let reveal = app.source_reveal_ticket(); let _ = app.update(old_target); assert_eq!( app.source_symbols.interaction, interaction, "old publication stays stale after modal closes" ); assert_eq!(app.source_reveal_ticket(), reveal); } let target = app.text_diagnostic_message(location(&app, 0, 7)).unwrap(); let _ = app.update(target); assert!( app.source_reveal_ticket().is_some(), "fresh admitted location still navigates" ); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.document.can_undo() && !app.document.can_redo()); } }