278 lines
11 KiB
Rust
278 lines
11 KiB
Rust
use super::*;
|
|
use crate::{context_menu::Entry, model_menu, views::traceability::Filter};
|
|
|
|
fn send(app: &mut App, message: Message) {
|
|
let _ = app.update(message);
|
|
}
|
|
|
|
fn fixture() -> (App, Id, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let oa = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
|
|
.unwrap();
|
|
let sa = p
|
|
.add_element(Kind::Function, Layer::Sa, "Detect", None)
|
|
.unwrap();
|
|
let mixed = p.ensure_diagram(Layer::Oa);
|
|
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
|
|
p.place(mixed, oa, Position { x: 300., y: 200. });
|
|
let system = p.ensure_diagram(Layer::Sa);
|
|
p.place(system, sa, Position { x: 700., y: 200. });
|
|
oa_core::validate_candidate(&p).unwrap();
|
|
let mut app = App::with_project(p);
|
|
app.page = Page::Diagram;
|
|
app.layer = Layer::Oa;
|
|
app.active_diagram = Some(mixed);
|
|
app.selected = oa;
|
|
app.refresh_draft();
|
|
app.traceability.filter = Filter::Artifacts;
|
|
app.traceability.query = "stale artifact query".into();
|
|
(app, mixed, system, oa, sa)
|
|
}
|
|
|
|
fn show_action(app: &mut App, element: Id) -> Message {
|
|
send(app, model_menu::open(element, iced::Point::ORIGIN));
|
|
model_menu::popup(app)
|
|
.unwrap()
|
|
.entries
|
|
.into_iter()
|
|
.find_map(|entry| match entry {
|
|
Entry::Action { label, message, .. } if label == "Show in this diagram" => {
|
|
Some(message)
|
|
}
|
|
_ => None,
|
|
})
|
|
.expect("eligible real menu action")
|
|
.expect("eligible menu action must be enabled")
|
|
}
|
|
|
|
#[test]
|
|
fn review_layer_traces_resets_inventory_without_applying_property_or_source_drafts() {
|
|
for source_draft in [false, true] {
|
|
let (mut app, mixed, _, oa, _) = fixture();
|
|
if source_draft {
|
|
send(&mut app, Message::Page(Page::Text));
|
|
send(
|
|
&mut app,
|
|
Message::SourceEdit(iced::widget::text_editor::Action::Edit(
|
|
iced::widget::text_editor::Edit::Insert('x'),
|
|
)),
|
|
);
|
|
assert!(app.source_dirty());
|
|
} else {
|
|
send(
|
|
&mut app,
|
|
Message::Field("/alias".into(), "invalid alias".into()),
|
|
);
|
|
assert!(app.draft_dirty());
|
|
}
|
|
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
|
let source = app.source_editor.text();
|
|
let before = app.document.snapshot();
|
|
let camera = (app.zoom, app.pan);
|
|
send(&mut app, Message::ReviewLayerTraces);
|
|
assert_eq!(app.page, Page::Traces);
|
|
assert_eq!(app.traceability.filter, Filter::Traces);
|
|
assert!(app.traceability.query.is_empty());
|
|
assert_eq!(
|
|
(app.layer, app.active_diagram, app.selected),
|
|
(Layer::Oa, Some(mixed), oa)
|
|
);
|
|
assert_eq!((app.zoom, app.pan), camera);
|
|
assert_eq!(
|
|
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
|
raw
|
|
);
|
|
assert_eq!(app.source_editor.text(), source);
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn review_layer_traces_respects_busy_modal_and_active_placement_guards() {
|
|
for already_traces in [false, true] {
|
|
for guard in 0..5 {
|
|
let (mut app, mixed, _, oa, _) = fixture();
|
|
match guard {
|
|
0 => app.busy = true,
|
|
1 => app.delete_pending = true,
|
|
2 => app.hosted.visible = true,
|
|
3 => send(
|
|
&mut app,
|
|
Message::Architecture(architecture_authoring::Event::Existing(None)),
|
|
),
|
|
4 => {
|
|
app.page = Page::Behavior;
|
|
let p = app.project().clone();
|
|
app.behavior
|
|
.creation
|
|
.update(&p, behavior::creation::Event::Open(Layer::Oa))
|
|
.unwrap();
|
|
assert!(app.behavior.has_pending());
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
if already_traces {
|
|
app.page = Page::Traces;
|
|
}
|
|
let original_page = app.page;
|
|
let before = app.document.snapshot();
|
|
let behavior_before = app.behavior.recovery_snapshot().unwrap();
|
|
send(&mut app, Message::ReviewLayerTraces);
|
|
assert_eq!(
|
|
app.page, original_page,
|
|
"guard {guard}, already traces {already_traces}"
|
|
);
|
|
assert_eq!(app.traceability.filter, Filter::Artifacts);
|
|
assert_eq!(app.traceability.query, "stale artifact query");
|
|
assert_eq!(
|
|
(app.layer, app.active_diagram, app.selected),
|
|
(Layer::Oa, Some(mixed), oa)
|
|
);
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), behavior_before);
|
|
if guard == 3 {
|
|
assert!(app.architecture.is_some());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_view_show_keeps_its_context_for_every_diagram_app_and_target_layer() {
|
|
for diagram_layer in [Layer::Oa, Layer::Sa] {
|
|
for app_layer in [Layer::Oa, Layer::Sa] {
|
|
for target_layer in [Layer::Oa, Layer::Sa] {
|
|
let mut p = Project::empty();
|
|
let oa = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
|
|
.unwrap();
|
|
let sa = p
|
|
.add_element(Kind::Function, Layer::Sa, "Detect", None)
|
|
.unwrap();
|
|
let target = if target_layer == Layer::Oa { oa } else { sa };
|
|
let other = if target_layer == Layer::Oa { sa } else { oa };
|
|
let mixed = p.ensure_diagram(diagram_layer);
|
|
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
|
|
// An attractive same-layer alternative makes accidental
|
|
// semantic-selection navigation observable in both directions.
|
|
let mut alternative = p.diagrams[&mixed].clone();
|
|
alternative.id = Id::new_v4();
|
|
alternative.alias = "same_layer_alternative".into();
|
|
alternative.layer = target_layer;
|
|
alternative.projection = ProjectionKind::Overview;
|
|
let alternative_id = alternative.id;
|
|
p.diagrams.insert(alternative_id, alternative);
|
|
p.place(alternative_id, target, Position { x: 700., y: 200. });
|
|
oa_core::validate_candidate(&p).unwrap();
|
|
let mut app = App::with_project(p);
|
|
app.page = Page::Diagram;
|
|
app.layer = app_layer;
|
|
app.active_diagram = Some(mixed);
|
|
app.selected = other;
|
|
app.refresh_draft();
|
|
let before = app.project().clone();
|
|
let action = show_action(&mut app, target);
|
|
send(&mut app, action);
|
|
// The existing RevealInDiagram action synchronizes the layer
|
|
// to the captured view, even if the prior perspective differed.
|
|
assert_eq!(
|
|
(app.active_diagram, app.layer, app.selected),
|
|
(Some(mixed), diagram_layer, target)
|
|
);
|
|
let added = app.project().diagrams[&mixed]
|
|
.nodes
|
|
.values()
|
|
.find(|n| n.element == target)
|
|
.unwrap()
|
|
.clone();
|
|
assert_eq!(app.selected_node, Some(added.id));
|
|
let mut expected = before;
|
|
expected
|
|
.diagrams
|
|
.get_mut(&mixed)
|
|
.unwrap()
|
|
.nodes
|
|
.insert(added.id, added);
|
|
assert_eq!(app.project(), &expected);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_view_show_menu_places_in_captured_view_with_exact_undo_redo() {
|
|
let (mut app, mixed, system, _, sa) = fixture();
|
|
let before = app.document.snapshot();
|
|
let action = show_action(&mut app, sa);
|
|
send(&mut app, action);
|
|
assert_eq!(
|
|
(app.active_diagram, app.layer, app.selected),
|
|
(Some(mixed), Layer::Oa, sa)
|
|
);
|
|
let added = app.project().diagrams[&mixed]
|
|
.nodes
|
|
.values()
|
|
.find(|n| n.element == sa)
|
|
.unwrap()
|
|
.clone();
|
|
assert_eq!(added.position, Position { x: 60., y: 60. });
|
|
let mut expected = before.decoded.project.clone();
|
|
expected
|
|
.diagrams
|
|
.get_mut(&mixed)
|
|
.unwrap()
|
|
.nodes
|
|
.insert(added.id, added);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(
|
|
app.project().diagrams[&system],
|
|
before.decoded.project.diagrams[&system]
|
|
);
|
|
let after = app.document.snapshot();
|
|
send(&mut app, Message::Undo);
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert!(!app.document.can_undo());
|
|
send(&mut app, Message::Redo);
|
|
assert_eq!(app.project(), &after.decoded.project);
|
|
assert_eq!(app.document.bundle(), &after.bundle);
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_view_show_menu_rejects_stale_context_and_pending_property_changes() {
|
|
for refusal in 0..3 {
|
|
let (mut app, mixed, system, oa, sa) = fixture();
|
|
let action = show_action(&mut app, sa);
|
|
match refusal {
|
|
0 => app.active_diagram = Some(system),
|
|
1 => app.canvas_epoch = app.canvas_epoch.wrapping_add(1),
|
|
2 => {
|
|
// A queued action must not apply a draft that became dirty
|
|
// after the menu was rendered, even when its context is valid.
|
|
app.draft
|
|
.as_mut()
|
|
.unwrap()
|
|
.input("/name", "Unapplied name".into());
|
|
assert!(app.draft_dirty());
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
let before = app.document.snapshot();
|
|
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
|
send(&mut app, action);
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
assert_eq!(app.selected, oa);
|
|
assert_eq!(
|
|
app.active_diagram,
|
|
Some(if refusal == 0 { system } else { mixed })
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
|
raw
|
|
);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
}
|