Files
ARCADIA-Studio/crates/oa-desktop/src/app/diagram_usages/tests.rs
T

1456 lines
51 KiB
Rust

use super::*;
use crate::source_navigation::{self as nav, query};
use oa_core::source_project::SourceBundle;
fn send(app: &mut App, message: Message) {
// Do not poll returned workers: completion below uses an independent Snapshot.
let _ = app.update(message);
}
fn lookup_completion(app: &App) -> usage::Completion {
let panel = app.diagram_usages.as_ref().expect("lookup panel");
assert!(panel.loading);
assert_eq!(app.diagram_lookup_in_flight, Some(panel.token));
let project = app.project().clone();
let process = app.behavior.topology.usage_snapshot().into_query_state();
let scenario = behavior::scenario::State::default();
usage::Completion {
token: panel.token,
context: panel.context,
result: Ok(usage::collect(
&project,
panel.context.subject,
&process,
&scenario,
)),
}
}
fn finish_lookup(app: &mut App) {
let completion = lookup_completion(app);
send(app, Message::DiagramUsagesReady(completion));
assert!(app.diagram_lookup_in_flight.is_none());
assert!(!app.diagram_usages.as_ref().unwrap().loading);
}
#[derive(Debug, PartialEq)]
struct AuthoredState {
project: Project,
accepted: SourceBundle,
drafts: SourceBundle,
revision: u64,
document_dirty: bool,
source_dirty: bool,
dirty: bool,
undo: bool,
redo: bool,
}
fn authored(app: &App) -> AuthoredState {
AuthoredState {
project: app.project().clone(),
accepted: app.document.bundle().clone(),
drafts: app.source_workspace.bundle(),
revision: app.document.revision(),
document_dirty: app.document.dirty(),
source_dirty: app.source_dirty(),
dirty: app.dirty(),
undo: app.document.can_undo(),
redo: app.document.can_redo(),
}
}
fn location(app: &App) -> (Page, Layer, Id, Option<Id>, Option<Id>, f32, iced::Vector) {
(
app.page,
app.layer,
app.selected,
app.selected_node,
app.active_diagram,
app.zoom,
app.pan,
)
}
fn saved_fixture() -> (App, [Id; 3], Id, Id) {
let mut p = Project::empty();
let first = p.add(Kind::Activity, "Repeated display name", 100_000., -50_000.);
let second = p.add(Kind::Activity, "Repeated display name", 100_420., -50_000.);
let unplaced = p
.add_element(Kind::Activity, Layer::Oa, "Repeated display name", None)
.unwrap();
let relation = p.relate(RelationKind::Interaction, "Precise handoff", first, second);
let diagram = p.ensure_diagram(Layer::Oa);
let mut app = App::with_project(p);
app.canvas_size = Some(iced::Size::new(800., 500.));
app.pan = iced::Vector::new(77., 33.);
app.zoom = 0.8;
(app, [first, second, unplaced], relation, diagram)
}
fn open(app: &mut App, subject: Id) -> (Id, Vec<Usage>) {
let message = app.diagram_usage_message(subject);
send(app, message);
assert!(app.diagram_usages.as_ref().unwrap().loading);
assert!(app.diagram_usages.as_ref().unwrap().usages.is_empty());
finish_lookup(app);
let panel = app
.diagram_usages
.as_ref()
.expect("navigation panel opened");
assert_eq!(panel.context.subject, subject);
(panel.token, panel.usages.clone())
}
fn choose(app: &mut App, token: Id, usage: Usage) {
send(app, Message::DiagramUsage(Event::Choose(token, usage)));
}
fn assert_centered(app: &App, bounds: iced::Rectangle) {
let viewport = app.canvas_size.unwrap();
assert!((bounds.center_x() * app.zoom + app.pan.x - viewport.width / 2.).abs() < 0.05);
assert!((bounds.center_y() * app.zoom + app.pan.y - viewport.height / 2.).abs() < 0.05);
assert!((0.5..=1.25).contains(&app.zoom));
assert!(!app.fit_to_view);
}
#[test]
fn saved_node_and_edge_navigation_preserve_whole_sources_and_actual_history() {
for edge in [false, true] {
let (mut app, elements, relation, diagram) = saved_fixture();
let original = app.document.snapshot();
let project = app.project().id;
app.document
.execute(Command::Rename(project, "First accepted edit".into()))
.unwrap();
let first = app.document.snapshot();
app.document
.execute(Command::Rename(project, "Redo remains available".into()))
.unwrap();
let second = app.document.snapshot();
assert!(app.document.undo());
app.sync_source();
app.refresh_draft();
let before = authored(&app);
assert!(before.undo && before.redo);
let subject = if edge { relation } else { elements[0] };
let (token, rows) = open(&mut app, subject);
assert_eq!(authored(&app), before);
let row = rows
.into_iter()
.find(|row| row.surface == Surface::Saved(diagram))
.unwrap();
let placement = match row.target {
Target::Placement(node) if !edge => Some(node),
Target::Edge(id) if edge && id == relation => None,
_ => panic!("wrong represented target"),
};
let bounds = row.bounds;
choose(&mut app, token, row);
assert!(app.diagram_usages.is_none());
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.active_diagram, Some(diagram));
assert_eq!(app.selected, subject);
assert_eq!(app.selected_node, placement);
assert_centered(&app, bounds);
assert_eq!(authored(&app), before);
assert!(Arc::ptr_eq(&first, &app.document.snapshot()));
assert!(app.document.undo());
assert_eq!(app.project(), &original.decoded.project);
assert_eq!(app.document.bundle(), &original.bundle);
assert!(app.document.redo());
assert_eq!(app.document.bundle(), &first.bundle);
assert!(app.document.redo());
assert_eq!(app.project(), &second.decoded.project);
assert_eq!(app.document.bundle(), &second.bundle);
}
}
#[test]
fn port_navigation_selects_port_and_exact_visible_owner_without_editing_geometry() {
let mut p = Project::empty();
let owner = p.add(Kind::Function, "Compute", 1200., 1500.);
let port = Id::new_v4();
let element = oa_core::ports::make_port(
&p,
port,
owner,
"Input",
oa_core::PortContract::FunctionInput {
incoming_items: vec![],
},
)
.unwrap();
p = execute_batch(&p, &[Command::CreateElement(element)]).unwrap();
let mut app = App::with_project(p);
app.canvas_size = Some(iced::Size::new(750., 480.));
let before = authored(&app);
let (token, rows) = open(&mut app, port);
assert_eq!(rows.len(), 1);
let row = rows[0].clone();
let Target::Port {
port: found,
placement,
} = row.target
else {
panic!("port representation");
};
assert_eq!(found, port);
assert_eq!(
app.project().diagrams[&row.surface.id()].nodes[&placement].element,
owner
);
let bounds = row.bounds;
choose(&mut app, token, row);
assert_eq!(app.selected, port);
assert_eq!(
app.selected_node, None,
"visual owner is not an edit/Delete selection"
);
let focus = app
.diagram_focus(Surface::Saved(app.active_diagram.unwrap()))
.unwrap();
assert_eq!(focus.target, Target::Port { port, placement });
assert_eq!(focus.bounds, bounds);
assert_eq!(app.layer, Layer::Sa);
assert_centered(&app, bounds);
assert_eq!(authored(&app), before);
}
#[test]
fn allocation_navigation_keeps_relation_selection_and_exact_visual_work_lifetime() {
let mut p = Project::empty();
let performer = p.add(Kind::Entity, "Responsible team", 1200., 1500.);
let work = p.add(Kind::Activity, "Assigned activity", 1500., 1650.);
let relation = p.relate(RelationKind::Allocation, "Responsibility", performer, work);
let diagram = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&diagram).unwrap().projection = oa_core::ProjectionKind::Allocation;
let work_placement = p.diagrams[&diagram]
.nodes
.values()
.find(|n| n.element == work)
.unwrap()
.id;
let performer_placement = p.diagrams[&diagram]
.nodes
.values()
.find(|n| n.element == performer)
.unwrap()
.id;
let mut app = App::with_project(p);
app.canvas_size = Some(iced::Size::new(800., 500.));
let before = authored(&app);
let (old_token, rows) = open(&mut app, relation);
assert_eq!(rows.len(), 1);
let row = rows[0].clone();
let expected = Target::AllocationGrouping {
relation,
work_placement,
performer_placement,
};
assert_eq!(row.target, expected);
choose(&mut app, old_token, row.clone());
assert_eq!(
app.selected, relation,
"later semantic actions must still target the Allocation"
);
assert_eq!(
app.selected_node, None,
"focus must not arm work-placement editing"
);
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().target,
expected
);
assert_centered(&app, row.bounds);
app.refresh_draft();
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().bounds,
row.bounds
);
let old_epoch = app.canvas_epoch;
let (token, _) = open(&mut app, relation);
choose(&mut app, token, row.clone());
assert_ne!(
app.canvas_epoch, old_epoch,
"equal-valued navigation retires gesture context"
);
let epoch = app.canvas_epoch;
choose(&mut app, old_token, row.clone());
assert_eq!(app.canvas_epoch, epoch);
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().target,
expected
);
for event in [
Event::Close(old_token),
Event::Search(old_token, "obsolete filter".into()),
] {
send(&mut app, Message::DiagramUsage(event));
assert!(
app.pending_diagram_reveal.is_some(),
"stale panel events cannot cancel measured reveal"
);
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().target,
expected
);
}
let (context, _, stamp) = app.pending_diagram_reveal.as_ref().unwrap();
let measured = Message::DiagramRevealViewport(*context, *stamp, iced::Size::new(900., 560.));
send(&mut app, measured);
assert!(app.pending_diagram_reveal.is_none());
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().target,
expected
);
assert_centered(&app, row.bounds);
assert_eq!(authored(&app), before);
for camera in [
Message::Pan(iced::Vector::new(40., -20.)),
Message::Zoom(1.1),
Message::Fit,
] {
send(&mut app, camera);
assert_eq!(app.selected, relation);
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().target,
expected
);
assert_eq!(authored(&app), before);
}
send(&mut app, Message::Select(work));
assert!(app.diagram_focus(Surface::Saved(diagram)).is_none());
let (token, _) = open(&mut app, relation);
choose(&mut app, token, row);
app.document
.execute(Command::Rename(work, "Accepted revision change".into()))
.unwrap();
assert!(app.diagram_focus(Surface::Saved(diagram)).is_none());
}
#[test]
fn state_row_navigation_has_exact_visual_row_without_retargeting_the_selected_relation() {
let mut p = Project::empty();
let machine = p
.add_element(Kind::StateMachine, Layer::Sa, "Modes", None)
.unwrap();
let region = p
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let state = p
.add_element(Kind::State, Layer::Sa, "Running", Some(region))
.unwrap();
let work = p
.add_element(Kind::Function, Layer::Sa, "Start monitoring", None)
.unwrap();
let relation = p.relate(RelationKind::StateAction, "Entry", state, work);
let diagram = p.ensure_diagram(Layer::Sa);
p.diagrams.get_mut(&diagram).unwrap().projection =
oa_core::ProjectionKind::StateMachine { machine };
let placement = p.place(diagram, state, Position { x: 1800., y: 2400. });
let mut app = App::with_project(p);
let before = authored(&app);
let (token, rows) = open(&mut app, relation);
let row = rows
.into_iter()
.find(|row| {
row.target
== (Target::StateRow {
placement,
relation,
})
})
.unwrap();
choose(&mut app, token, row.clone());
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.behavior.active_owner, Some(machine));
assert_eq!(app.selected, relation);
assert_eq!(app.selected_node, None);
let focus = app.diagram_focus(Surface::Saved(diagram)).unwrap();
assert_eq!(
focus.target,
Target::StateRow {
placement,
relation
}
);
assert_eq!(focus.bounds, row.bounds);
app.refresh_draft();
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().bounds,
row.bounds
);
assert_eq!(authored(&app), before);
let context = app.state_context().unwrap();
send(
&mut app,
Message::StateDiagram(behavior::state_edit::Event::Pan(
context,
iced::Vector::new(35., 20.),
)),
);
assert_eq!(app.selected, relation);
assert_eq!(
app.diagram_focus(Surface::Saved(diagram)).unwrap().target,
row.target
);
assert_eq!(authored(&app), before);
app.canvas_document = Id::new_v4();
assert!(app.diagram_focus(Surface::Saved(diagram)).is_none());
}
#[test]
fn accepted_new_chooser_retires_old_measurement_without_blocking_future_viewports() {
let (mut app, elements, _, diagram) = saved_fixture();
let (token, rows) = open(&mut app, elements[0]);
choose(&mut app, token, rows[0].clone());
assert!(app.pending_diagram_reveal.is_some());
let (new_token, _) = open(&mut app, elements[1]);
assert!(app.pending_diagram_reveal.is_none());
assert!(app.diagram_focus(Surface::Saved(diagram)).is_none());
send(&mut app, Message::DiagramUsage(Event::Close(new_token)));
let size = iced::Size::new(945., 615.);
send(&mut app, Message::CanvasResized(size));
assert_eq!(app.canvas_size, Some(size));
}
#[test]
fn tree_origin_captures_subject_and_never_falls_back_to_current_selection_or_name() {
let (mut app, elements, _, _) = saved_fixture();
let captured = app.diagram_usage_message(elements[0]);
send(&mut app, Message::Select(elements[1]));
let before = authored(&app);
send(&mut app, captured);
finish_lookup(&mut app);
let panel = app.diagram_usages.as_ref().unwrap();
assert_eq!(panel.context.subject, elements[0]);
assert!(panel.usages.iter().all(|row| row.subject == elements[0]));
assert_eq!(app.selected, elements[1]);
let token = panel.token;
send(&mut app, Message::DiagramUsage(Event::Close(token)));
let (_, rows) = open(&mut app, elements[2]);
assert!(rows.is_empty(), "an unplaced homonym has no representation");
assert_eq!(authored(&app), before);
}
#[test]
fn old_panel_tokens_and_forged_targets_cannot_redirect_selection() {
let (mut app, elements, _, _) = saved_fixture();
let (old_token, old_rows) = open(&mut app, elements[0]);
let (token, rows) = open(&mut app, elements[1]);
assert_ne!(token, old_token);
let before = authored(&app);
let prior = location(&app);
choose(&mut app, old_token, old_rows[0].clone());
assert_eq!(app.diagram_usages.as_ref().unwrap().token, token);
assert_eq!(location(&app), prior);
// Even a current panel token cannot authorize another subject's valid row.
choose(&mut app, token, old_rows[0].clone());
assert_eq!(location(&app), prior);
let refreshed = app.diagram_usages.as_ref().unwrap().token;
assert_ne!(refreshed, token);
let mut forged = rows[0].clone();
forged.target = old_rows[0].target.clone();
choose(&mut app, refreshed, forged);
assert_eq!(location(&app), prior);
assert_eq!(authored(&app), before);
}
#[test]
fn revision_retarget_and_document_change_make_old_destinations_inert() {
let (mut app, elements, _, diagram) = saved_fixture();
let (token, rows) = open(&mut app, elements[0]);
let old_row = rows[0].clone();
let Target::Placement(node) = old_row.target else {
panic!("node target");
};
let mut replacement = app.project().diagrams[&diagram].nodes[&node].clone();
replacement.element = elements[2];
app.document
.execute(Command::EditNode {
diagram,
original: node,
node: replacement,
})
.unwrap();
app.sync_source();
app.refresh_draft();
let before = authored(&app);
let prior = location(&app);
choose(&mut app, token, old_row.clone());
let panel = app.diagram_usages.as_ref().unwrap();
assert_ne!(panel.token, token);
assert_eq!(panel.context.revision, app.document.revision());
assert!(panel.usages.is_empty());
assert!(panel.notice.is_some());
let fresh_token = panel.token;
assert!(
panel.loading,
"revision refresh starts a worker, never activates the old event"
);
finish_lookup(&mut app);
choose(&mut app, fresh_token, old_row);
assert_eq!(location(&app), prior);
assert_eq!(authored(&app), before);
let token = app.diagram_usages.as_ref().unwrap().token;
app.canvas_document = Id::new_v4();
send(
&mut app,
Message::DiagramUsage(Event::Search(token, "stale".into())),
);
assert!(app.diagram_usages.is_none());
assert_eq!(location(&app), prior);
assert_eq!(authored(&app), before);
}
#[test]
fn stale_open_context_cannot_replace_a_panel() {
let (mut app, elements, _, _) = saved_fixture();
let (token, _) = open(&mut app, elements[0]);
let context = app.diagram_usages.as_ref().unwrap().context;
for stale in [
Context {
document: Id::new_v4(),
..context
},
Context {
revision: context.revision.wrapping_add(1),
..context
},
Context {
subject: Id::new_v4(),
..context
},
] {
send(&mut app, Message::DiagramUsage(Event::Open(stale)));
assert_eq!(app.diagram_usages.as_ref().unwrap().token, token);
}
}
#[test]
fn pending_source_behavior_and_modal_inputs_block_open_and_choose_without_loss() {
for opened in [false, true] {
for block in 0..8 {
let (mut app, elements, _, _) = saved_fixture();
let choice = opened.then(|| {
let (token, rows) = open(&mut app, elements[0]);
(token, rows[0].clone())
});
match block {
0 => app.busy = true,
1 => app.hosted.visible = true,
2 => app.pending = Some(Pending::Walkthrough),
3 => {
app.file_prompt = Some(FilePrompt {
operation: FileOperation::Save,
path: "keep-this-path.arcadia".into(),
overwrite: false,
})
}
4 => {
let cursor = app.source_editor.cursor();
app.source_workspace
.edit("? retain this invalid source draft".into(), cursor);
}
5 => {
let project = app.project().clone();
assert!(
app.behavior
.creation
.update(&project, behavior::creation::Event::Open(Layer::Oa))
.unwrap()
.is_none()
);
}
6 => app.delete_pending = true,
7 => {
app.draft
.as_mut()
.unwrap()
.input("/name", "Unapplied inspector name".into());
assert!(app.draft_dirty());
}
_ => unreachable!(),
}
let before = authored(&app);
let draft_before = app
.draft
.as_ref()
.map(|draft| serde_json::to_value(draft.recovery_snapshot()).unwrap());
let prior = location(&app);
if let Some((token, row)) = choice {
choose(&mut app, token, row);
} else {
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
}
assert_eq!(authored(&app), before, "opened={opened}, block={block}");
assert_eq!(location(&app), prior);
assert_eq!(
app.draft
.as_ref()
.map(|draft| serde_json::to_value(draft.recovery_snapshot()).unwrap()),
draft_before
);
assert_eq!(app.diagram_usages.is_some(), opened);
if block == 3 {
assert_eq!(
app.file_prompt.as_ref().unwrap().path,
"keep-this-path.arcadia"
);
}
if block == 5 {
assert!(app.behavior.has_pending());
}
}
}
}
fn caret_on_alias(app: &mut App, subject: Id) {
let alias = usage::alias(app.project(), subject).to_owned();
let (path, span) = app
.source_workspace
.files
.iter()
.find_map(|(path, buffer)| {
let outline = language::outline(
&buffer.text,
language::OutlineLimits {
max_source_bytes: usize::MAX,
max_tokens: usize::MAX,
max_declarations: usize::MAX,
},
)
.unwrap();
outline
.declarations
.iter()
.find(|declaration| {
declaration.declaration.id == Some(subject)
&& declaration.declaration.alias == alias
})
.map(|declaration| (path.clone(), declaration.alias_span))
})
.unwrap();
app.page = Page::Text;
send(app, Message::SourceFile(path));
if let Some(ticket) = app.source_reveal_ticket() {
send(app, Message::SourceViewport(Some(ticket)));
}
let text = app.source_workspace.text();
let prefix = &text[..span.start];
let line = prefix.bytes().filter(|byte| *byte == b'\n').count();
let column = prefix
.rsplit_once('\n')
.map_or(prefix.len(), |(_, tail)| tail.len());
app.source_editor.move_to(text_editor::Cursor {
position: text_editor::Position { line, column },
selection: None,
});
}
fn begin_caret(app: &mut App) -> Option<nav::Request> {
let message = app.source_symbol_message(nav::SymbolEvent::DiagramUsages);
send(app, message);
app.source_symbols.request.clone()
}
fn complete_caret(app: &App, request: nav::Request) -> nav::Completion {
let drafts = app.source_workspace.bundle();
let accepted = app.document.bundle();
let files: Vec<_> = drafts
.files
.iter()
.map(|(path, text)| query::DraftFile { path, text })
.collect();
let validated: Vec<_> = accepted
.files
.iter()
.map(|(path, text)| query::DraftFile { path, text })
.collect();
let snapshot = Arc::new(
query::capture(
query::Input {
key: request.stamp.snapshot,
entry: &drafts.entry,
files: &files,
applied: app.source_workspace.applied,
validation: Some(query::ValidatedSources {
entry: &accepted.entry,
files: &validated,
}),
},
query::Limits::default(),
)
.unwrap(),
);
let results = Arc::new(
snapshot
.query(query::Query {
stamp: request.stamp,
path: &request.path,
byte_offset: request.offset,
action: request.action,
})
.unwrap(),
);
nav::Completion {
request,
result: Ok((snapshot, results)),
}
}
#[test]
fn caret_uses_exact_verified_alias_identity_and_preserves_source_cursor() {
let (mut app, elements, _, _) = saved_fixture();
send(&mut app, Message::Select(elements[1]));
caret_on_alias(&mut app, elements[0]);
let before = authored(&app);
let cursor = app.source_editor.cursor();
let source = app.source_workspace.active.clone();
let request = begin_caret(&mut app).unwrap();
let ready = complete_caret(&app, request);
let (_, results) = ready.result.as_ref().unwrap();
assert_eq!(results.disposition, query::Disposition::Direct);
assert!(results.scope.validated);
assert_eq!(results.definitions.len(), 1);
assert_eq!(
results.definitions[0].declaration.authored_id,
Some(elements[0])
);
send(&mut app, Message::SourceSymbolsReady(ready));
assert_eq!(
app.diagram_usages.as_ref().unwrap().context.subject,
elements[0]
);
assert_eq!(
app.selected, elements[1],
"opening must not first jump to another source/model selection"
);
assert_eq!(app.source_workspace.active, source);
assert_eq!(app.source_editor.cursor(), cursor);
assert_eq!(authored(&app), before);
}
#[test]
fn dirty_or_comment_caret_has_no_selected_object_fallback() {
for dirty in [false, true] {
let (mut app, elements, _, _) = saved_fixture();
let mut bundle = app.document.bundle().clone();
let entry = bundle.entry.clone();
bundle
.files
.get_mut(&entry)
.unwrap()
.insert_str(0, "// @not_a_live_alias\n");
app.document = Document::from_bundle(bundle, LIMITS).unwrap();
app.source_workspace = Workspace::new(app.document.bundle(), true);
app.page = Page::Text;
app.selected = elements[0];
app.refresh_draft();
app.show_source_file(false);
app.source_editor.move_to(text_editor::Cursor {
position: text_editor::Position { line: 0, column: 6 },
selection: None,
});
if dirty {
let cursor = app.source_editor.cursor();
app.source_workspace
.edit("? invalid draft @not_a_live_alias".into(), cursor);
}
let before = authored(&app);
if let Some(request) = begin_caret(&mut app) {
assert!(!dirty, "dirty source must not start model lookup");
let ready = complete_caret(&app, request);
assert_eq!(
ready.result.as_ref().unwrap().1.disposition,
query::Disposition::NoSymbol
);
send(&mut app, Message::SourceSymbolsReady(ready));
} else {
assert!(dirty);
}
assert!(app.diagram_usages.is_none());
assert_eq!(app.selected, elements[0]);
assert!(app.source_symbols.notice.is_some());
assert_eq!(authored(&app), before);
}
}
#[test]
fn stale_caret_completion_never_opens_diagrams_for_current_selection() {
for stale in 0..4 {
let (mut app, elements, _, _) = saved_fixture();
caret_on_alias(&mut app, elements[0]);
let request = begin_caret(&mut app).unwrap();
let ready = complete_caret(&app, request);
match stale {
0 => send(
&mut app,
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Insert('x'))),
),
1 => app.source_workspace.reset_navigation_session(),
2 => {
app.source_symbols
.raw_interaction
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
3 => app.source_editor.move_to(text_editor::Cursor {
position: text_editor::Position { line: 0, column: 0 },
selection: None,
}),
_ => unreachable!(),
}
let before = authored(&app);
let prior = location(&app);
send(&mut app, Message::SourceSymbolsReady(ready));
assert!(app.source_symbols.in_flight.is_none());
assert!(app.diagram_usages.is_none());
assert_eq!(location(&app), prior);
assert_eq!(authored(&app), before);
}
}
fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
let mut element = p
.make_element(
Kind::Occurrence,
p.elements[&owner].layer(),
name,
Some(owner),
)
.unwrap();
let id = element.id;
let Some(SharedBody::Occurrence { body: value, .. }) = element.shared_mut() else {
panic!("occurrence");
};
*value = body;
*p = execute_batch(p, &[Command::CreateElement(element)]).unwrap();
id
}
#[test]
fn repeated_process_calls_reveal_the_chosen_caller_and_distinct_tile_camera() {
let fixture = behavior::topology::direct_tests::fixture(Layer::Oa);
let mut project = fixture.p;
let reused = project
.make_element(Kind::Process, Layer::Oa, "Repeated reusable check", None)
.unwrap();
let reused_id = reused.id;
project = execute_batch(&project, &[Command::CreateElement(reused)]).unwrap();
let leaf = add_occurrence(
&mut project,
reused_id,
"Check",
OccurrenceBody::Action {
action: Some(fixture.work[0]),
lifeline: None,
},
);
let first = add_occurrence(
&mut project,
fixture.owner,
"First call",
OccurrenceBody::SequenceReference {
sequence: Some(reused_id),
},
);
let second = add_occurrence(
&mut project,
fixture.owner,
"Second call",
OccurrenceBody::SequenceReference {
sequence: Some(reused_id),
},
);
let mut app = App::with_project(project);
app.behavior.topology.viewport(iced::Size::new(850., 460.));
let before = authored(&app);
let (token, rows) = open(&mut app, leaf);
let row = rows
.iter()
.find(|row| {
row.surface == Surface::Process(fixture.owner)
&& row.target
== Target::ProcessOccurrence(OccurrenceEndpoint {
references: vec![first],
occurrence: leaf,
})
})
.unwrap()
.clone();
choose(&mut app, token, row);
assert_eq!(app.selected, leaf);
let context = app.topology_context().unwrap();
send(
&mut app,
Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(
context,
iced::Size::new(850., 460.),
)),
);
assert!(app.pending_diagram_reveal.is_none());
assert_eq!(
app.behavior.topology.selection_address().unwrap().endpoint,
OccurrenceEndpoint {
references: vec![first],
occurrence: leaf
}
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
Some(&Target::ProcessOccurrence(OccurrenceEndpoint {
references: vec![first],
occurrence: leaf
}))
);
assert!(app.process_preview_primary().is_some());
assert!(app.draft.is_none());
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.behavior.active_owner, Some(fixture.owner));
let first_camera = app.behavior.topology.camera_boundary();
let (token, rows) = open(&mut app, leaf);
let row = rows
.into_iter()
.find(|row| {
row.surface == Surface::Process(fixture.owner)
&& row.target
== Target::ProcessOccurrence(OccurrenceEndpoint {
references: vec![second],
occurrence: leaf,
})
})
.unwrap();
choose(&mut app, token, row);
assert_eq!(app.selected, leaf);
let context = app.topology_context().unwrap();
send(
&mut app,
Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(
context,
iced::Size::new(850., 460.),
)),
);
assert!(app.pending_diagram_reveal.is_none());
assert_eq!(app.behavior.active_owner, Some(fixture.owner));
assert_eq!(
app.behavior.topology.selection_address().unwrap().endpoint,
OccurrenceEndpoint {
references: vec![second],
occurrence: leaf
}
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
Some(&Target::ProcessOccurrence(OccurrenceEndpoint {
references: vec![second],
occurrence: leaf
}))
);
assert!(app.process_preview_primary().is_some());
assert!(app.draft.is_none());
assert_eq!(app.selected_node, None);
assert!(app.behavior.topology.camera_boundary() != first_camera);
assert_eq!(authored(&app), before);
}
#[test]
fn repeated_scenario_messages_select_the_exact_occurrence_and_move_camera() {
let fixture = behavior::scenario::direct_tests::fixture();
let mut app = App::with_project(fixture.project);
app.behavior.scenario.viewport(iced::Size::new(850., 460.));
let before = authored(&app);
let (token, rows) = open(&mut app, fixture.exchange);
let call = rows
.iter()
.find(|row| {
row.surface == Surface::Scenario(fixture.owner)
&& row.target == Target::ScenarioObject(fixture.call)
})
.unwrap()
.clone();
let reply = rows
.iter()
.find(|row| {
row.surface == Surface::Scenario(fixture.owner)
&& row.target == Target::ScenarioObject(fixture.reply)
})
.unwrap()
.clone();
assert_ne!(call.bounds, reply.bounds);
choose(&mut app, token, call);
assert_eq!(app.selected, fixture.call);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.behavior.active_owner, Some(fixture.owner));
let first_camera = app.behavior.scenario.camera_boundary();
let (token, _) = open(&mut app, fixture.exchange);
choose(&mut app, token, reply);
assert_eq!(app.selected, fixture.reply);
assert_eq!(app.selected_node, None);
assert!(app.behavior.scenario.camera_boundary() != first_camera);
assert_eq!(authored(&app), before);
}
#[test]
fn repeated_preview_arrows_select_the_relation_and_highlight_the_exact_caller_link() {
let fixture = behavior::topology::direct_tests::fixture(Layer::Oa);
let mut p = fixture.p;
let reused = p
.make_element(Kind::Process, Layer::Oa, "Reusable sequence", None)
.unwrap();
let sequence = reused.id;
p = execute_batch(&p, &[Command::CreateElement(reused)]).unwrap();
let leaves: Vec<_> = ["First check", "Next check"]
.into_iter()
.map(|name| {
add_occurrence(
&mut p,
sequence,
name,
OccurrenceBody::Action {
action: Some(fixture.work[0]),
lifeline: None,
},
)
})
.collect();
let calls: Vec<_> = ["First invocation", "Second invocation"]
.into_iter()
.map(|name| {
add_occurrence(
&mut p,
fixture.owner,
name,
OccurrenceBody::SequenceReference {
sequence: Some(sequence),
},
)
})
.collect();
let relation = p.relate(
RelationKind::SequenceLink,
"Next check",
leaves[0],
leaves[1],
);
let mut app = App::with_project(p);
app.behavior.topology.viewport(iced::Size::new(850., 460.));
let before = authored(&app);
let mut previous_camera = None;
let mut previous_epoch = None;
for call in [calls[0], calls[1], calls[1]] {
let (token, rows) = open(&mut app, relation);
assert_eq!(
rows.iter()
.filter(|row| row.surface == Surface::Process(fixture.owner))
.count(),
2
);
assert!(
rows.iter()
.any(|row| row.surface == Surface::Process(sequence)
&& row.target == Target::ProcessLink(relation))
);
let target = Target::ProcessPreviewLink {
relation,
source: OccurrenceEndpoint {
references: vec![call],
occurrence: leaves[0],
},
target: OccurrenceEndpoint {
references: vec![call],
occurrence: leaves[1],
},
};
let row = rows.into_iter().find(|row| row.target == target).unwrap();
assert!(usage::target_name(app.project(), &row).contains(app.project().named(call)));
choose(&mut app, token, row.clone());
assert_eq!(app.selected, relation);
assert_eq!(app.selected_node, None);
assert_eq!(app.behavior.active_owner, Some(fixture.owner));
let focus = app.diagram_focus(Surface::Process(fixture.owner)).unwrap();
assert_eq!(focus.target, target);
assert_eq!(focus.bounds, row.bounds);
assert_eq!(
behavior::selected_owner(&app),
Some(fixture.owner),
"renderer must remain in the chosen caller"
);
let camera = app.behavior.topology.camera_boundary();
if call == calls[1]
&& let Some((previous_call, previous)) = &previous_camera
&& *previous_call != call
{
assert!(&camera != previous);
}
if let Some(epoch) = previous_epoch {
assert_ne!(app.canvas_epoch, epoch);
}
previous_camera = Some((call, camera));
previous_epoch = Some(app.canvas_epoch);
assert_eq!(authored(&app), before);
}
let context = behavior::topology::Context {
document: app.canvas_document,
owner: fixture.owner,
revision: app.document.revision(),
epoch: app.canvas_epoch,
};
let camera_epoch = app.camera_epoch;
send(
&mut app,
Message::TopologyCanvas(behavior::topology::CanvasEvent::Camera(
context,
camera_epoch,
behavior::canvas_workspace::Event::Pan(iced::Vector::new(50., 30.)),
)),
);
assert_eq!(app.selected, relation);
assert!(
matches!(&app.diagram_focus(Surface::Process(fixture.owner)).unwrap().target,
Target::ProcessPreviewLink { source, .. } if source.references == vec![calls[1]])
);
assert_eq!(authored(&app), before);
let (token, rows) = open(&mut app, relation);
let row = rows
.into_iter()
.find(|row| {
matches!(&row.target,
Target::ProcessPreviewLink { source, .. } if source.references == vec![calls[1]])
})
.unwrap();
let mut changed = app.project().elements[&calls[1]].clone();
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::SequenceReference { sequence },
..
}) = changed.shared_mut()
{
*sequence = None;
}
app.document
.execute(Command::EditElement(calls[1], changed))
.unwrap();
app.sync_source();
app.refresh_draft();
let prior = app.behavior.topology.camera_boundary();
choose(&mut app, token, row);
assert!(
app.behavior.topology.camera_boundary() == prior,
"stale retargeted arrow cannot jump"
);
assert!(app.diagram_focus(Surface::Process(fixture.owner)).is_none());
}
#[test]
fn asynchronous_lookup_is_loading_inert_and_retains_filter_until_completion() {
let (mut app, elements, _, _) = saved_fixture();
let before = authored(&app);
let prior = location(&app);
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let ready = lookup_completion(&app);
let row = ready.result.as_ref().unwrap()[0].clone();
let token = ready.token;
let panel = app.diagram_usages.as_ref().unwrap();
assert!(panel.loading && panel.usages.is_empty());
assert!(panel.choices(app.project()).is_empty());
send(
&mut app,
Message::DiagramUsage(Event::Search(token, "Repeated".into())),
);
send(&mut app, Message::DiagramUsage(Event::Move(token, 1)));
choose(&mut app, token, row);
assert_eq!(app.diagram_lookup_in_flight, Some(token));
assert!(app.diagram_usages.as_ref().unwrap().loading);
assert_eq!(location(&app), prior);
send(&mut app, Message::DiagramUsagesReady(ready));
let panel = app.diagram_usages.as_ref().unwrap();
assert!(!panel.loading && !panel.usages.is_empty());
assert_eq!(panel.search, "Repeated");
assert_eq!(panel.cursor, 0);
assert!(app.diagram_lookup_in_flight.is_none());
assert_eq!(location(&app), prior);
assert_eq!(authored(&app), before);
}
#[test]
fn closed_lookup_keeps_slot_and_replacement_waits_for_exact_old_completion() {
let (mut app, elements, _, _) = saved_fixture();
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let old = lookup_completion(&app);
send(&mut app, Message::DiagramUsage(Event::Close(old.token)));
assert!(app.diagram_usages.is_none());
assert_eq!(app.diagram_lookup_in_flight, Some(old.token));
let message = app.diagram_usage_message(elements[1]);
send(&mut app, message);
let replacement = app.diagram_usages.as_ref().unwrap().token;
assert_ne!(replacement, old.token);
assert!(app.diagram_usages.as_ref().unwrap().loading);
assert_eq!(
app.diagram_lookup_in_flight,
Some(old.token),
"no overlapping worker"
);
let before = authored(&app);
send(&mut app, Message::DiagramUsagesReady(old.clone()));
assert_eq!(app.diagram_lookup_in_flight, Some(replacement));
assert!(app.diagram_usages.as_ref().unwrap().usages.is_empty());
send(&mut app, Message::DiagramUsagesReady(old));
assert_eq!(
app.diagram_lookup_in_flight,
Some(replacement),
"duplicate cannot release new worker"
);
finish_lookup(&mut app);
let panel = app.diagram_usages.as_ref().unwrap();
assert_eq!(panel.context.subject, elements[1]);
assert!(panel.usages.iter().all(|row| row.subject == elements[1]));
assert_eq!(authored(&app), before);
}
#[test]
fn completion_after_cancel_releases_slot_without_reopening_or_moving() {
let (mut app, elements, _, _) = saved_fixture();
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let ready = lookup_completion(&app);
send(&mut app, Message::Escape);
assert!(app.diagram_usages.is_none());
assert_eq!(app.diagram_lookup_in_flight, Some(ready.token));
let before = authored(&app);
let prior = location(&app);
send(&mut app, Message::DiagramUsagesReady(ready));
assert!(app.diagram_lookup_in_flight.is_none());
assert!(app.diagram_usages.is_none());
assert_eq!(authored(&app), before);
assert_eq!(location(&app), prior);
}
#[test]
fn forged_completion_cannot_release_another_slot_or_admit_wrong_context() {
let (mut app, elements, _, _) = saved_fixture();
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let ready = lookup_completion(&app);
let before = authored(&app);
let prior = location(&app);
let mut wrong_job = ready.clone();
wrong_job.token = Id::new_v4();
send(&mut app, Message::DiagramUsagesReady(wrong_job));
assert_eq!(app.diagram_lookup_in_flight, Some(ready.token));
assert!(app.diagram_usages.as_ref().unwrap().loading);
let mut wrong_context = ready.clone();
wrong_context.context.subject = elements[1];
send(&mut app, Message::DiagramUsagesReady(wrong_context));
assert!(
app.diagram_lookup_in_flight.is_none(),
"the exact slot is released even for invalid context"
);
let panel = app.diagram_usages.as_ref().unwrap();
assert!(panel.usages.is_empty() && !panel.loading && panel.notice.is_some());
send(&mut app, Message::DiagramUsagesReady(ready));
assert!(app.diagram_usages.as_ref().unwrap().usages.is_empty());
assert_eq!(authored(&app), before);
assert_eq!(location(&app), prior);
}
#[test]
fn stale_revision_completion_refreshes_asynchronously_without_activating() {
let (mut app, elements, _, _) = saved_fixture();
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let old = lookup_completion(&app);
app.document
.execute(Command::Rename(elements[0], "Updated accepted name".into()))
.unwrap();
app.sync_source();
app.refresh_draft();
let before = authored(&app);
let prior = location(&app);
send(&mut app, Message::DiagramUsagesReady(old.clone()));
let panel = app.diagram_usages.as_ref().unwrap();
assert_ne!(panel.token, old.token);
assert_eq!(panel.context.revision, app.document.revision());
assert!(panel.loading && panel.usages.is_empty());
assert_eq!(app.diagram_lookup_in_flight, Some(panel.token));
finish_lookup(&mut app);
assert_eq!(location(&app), prior);
assert_eq!(authored(&app), before);
}
#[test]
fn reset_keeps_old_worker_slot_and_only_new_document_results_are_admitted() {
let (mut app, elements, _, _) = saved_fixture();
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let old = lookup_completion(&app);
let mut project = Project::empty();
let subject = project.add(Kind::Activity, "New document", 30., 40.);
app.reset(project, None);
assert_ne!(app.canvas_document, old.context.document);
assert_eq!(app.diagram_lookup_in_flight, Some(old.token));
assert!(app.diagram_usages.is_none());
let message = app.diagram_usage_message(subject);
send(&mut app, message);
let token = app.diagram_usages.as_ref().unwrap().token;
assert_eq!(app.diagram_lookup_in_flight, Some(old.token));
let before = authored(&app);
send(&mut app, Message::DiagramUsagesReady(old));
assert_eq!(app.diagram_lookup_in_flight, Some(token));
assert!(app.diagram_usages.as_ref().unwrap().usages.is_empty());
finish_lookup(&mut app);
let panel = app.diagram_usages.as_ref().unwrap();
assert_eq!(panel.context.document, app.canvas_document);
assert_eq!(panel.context.subject, subject);
assert!(panel.usages.iter().all(|row| row.subject == subject));
assert_eq!(authored(&app), before);
}
#[test]
fn worker_failure_finishes_loading_and_can_be_retried_without_losing_sources() {
let (mut app, elements, _, _) = saved_fixture();
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let mut completion = lookup_completion(&app);
completion.result = Err("Injected worker failure".into());
let token = completion.token;
let before = authored(&app);
send(&mut app, Message::DiagramUsagesReady(completion));
assert!(app.diagram_lookup_in_flight.is_none());
let panel = app.diagram_usages.as_ref().unwrap();
assert!(!panel.loading && panel.usages.is_empty());
assert_eq!(panel.notice.as_deref(), Some("Injected worker failure"));
send(&mut app, Message::DiagramUsage(Event::Close(token)));
let (_, rows) = open(&mut app, elements[0]);
assert!(!rows.is_empty());
assert_eq!(authored(&app), before);
}
#[test]
fn oversized_node_focus_stays_on_label_and_single_surface_query_excludes_other_views() {
let (mut app, elements, _, diagram) = saved_fixture();
let node = app.project().diagrams[&diagram]
.nodes
.values()
.find(|node| node.element == elements[0])
.unwrap()
.id;
let mut expanded = app.project().diagrams[&diagram].nodes[&node].clone();
expanded.size.width = 9000.;
expanded.size.height = 8000.;
app.document
.execute(Command::EditNode {
diagram,
original: node,
node: expanded.clone(),
})
.unwrap();
app.sync_source();
app.refresh_draft();
let before = authored(&app);
let hits = usage::saved_usages_for(app.project(), elements[0], Some(diagram));
assert_eq!(hits.len(), 1);
assert!(hits[0].bounds.width <= 420. && hits[0].bounds.height <= 64.);
assert_eq!(
(hits[0].bounds.x, hits[0].bounds.y),
(expanded.position.x, expanded.position.y)
);
assert!(usage::saved_usages_for(app.project(), elements[0], Some(Id::new_v4())).is_empty());
assert!(
hits.iter()
.all(|hit| hit.surface == Surface::Saved(diagram))
);
assert_eq!(authored(&app), before);
}
#[test]
fn query_presentation_snapshot_keeps_expansion_without_retaining_live_state() {
let fixture = behavior::topology::direct_tests::fixture(Layer::Oa);
let mut project = fixture.p;
let reused = project
.make_element(Kind::Process, Layer::Oa, "Long reusable check", None)
.unwrap();
let reused_id = reused.id;
project = execute_batch(&project, &[Command::CreateElement(reused)]).unwrap();
let mut leaves = Vec::new();
for n in 0..5 {
leaves.push(add_occurrence(
&mut project,
reused_id,
&format!("Check {n}"),
OccurrenceBody::Action {
action: Some(fixture.work[0]),
lifeline: None,
},
));
}
let call = add_occurrence(
&mut project,
fixture.owner,
"Expanded call",
OccurrenceBody::SequenceReference {
sequence: Some(reused_id),
},
);
let mut live = behavior::topology::State::default();
live.enter(Id::new_v4(), fixture.owner);
assert!(live.expand(&project, fixture.owner, call));
let snapshot = live.usage_snapshot();
assert!(live.expand(&project, fixture.owner, call));
assert!(
live.diagram_usages(&project, fixture.owner, leaves[4])
.is_empty()
);
let query_state = snapshot.into_query_state();
let rows = query_state.diagram_usages(&project, fixture.owner, leaves[4]);
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].target,
Target::ProcessOccurrence(OccurrenceEndpoint {
references: vec![call],
occurrence: leaves[4],
})
);
}
#[test]
fn whole_surface_camera_fits_huge_extent_while_target_camera_remains_readable() {
let bounds = iced::Rectangle {
x: -20_000.,
y: -30_000.,
width: 100_000.,
height: 80_000.,
};
let viewport = iced::Size::new(800., 500.);
let (zoom, pan) = usage::camera_for(&Target::Whole, bounds, viewport);
assert!((0.0001..0.05).contains(&zoom));
assert!(bounds.width * zoom <= viewport.width - 64. + 0.01);
assert!(bounds.height * zoom <= viewport.height - 64. + 0.01);
assert!((bounds.center_x() * zoom + pan.x - viewport.width / 2.).abs() < 0.01);
assert!((bounds.center_y() * zoom + pan.y - viewport.height / 2.).abs() < 0.01);
let focused = usage::camera_for(&Target::Placement(Id::new_v4()), bounds, viewport);
assert_eq!(focused, usage::camera(bounds, viewport));
}
#[test]
fn completion_releases_worker_behind_busy_modal_without_navigation() {
let (mut app, elements, _, _) = saved_fixture();
let message = app.diagram_usage_message(elements[0]);
send(&mut app, message);
let ready = lookup_completion(&app);
app.busy = true;
app.hosted.visible = true;
let before = authored(&app);
let prior = location(&app);
send(&mut app, Message::DiagramUsagesReady(ready));
assert!(app.diagram_lookup_in_flight.is_none());
assert!(!app.diagram_usages.as_ref().unwrap().loading);
assert!(app.busy && app.hosted.visible);
assert_eq!(location(&app), prior);
assert_eq!(authored(&app), before);
}
mod saved_scenario;