1456 lines
51 KiB
Rust
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;
|