878 lines
34 KiB
Rust
878 lines
34 KiB
Rust
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::<Vec<_>>();
|
|
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());
|
|
}
|
|
}
|