Prepare complete saved Scenario display fallback adapter
This commit is contained in:
@@ -23,7 +23,7 @@ pub(crate) mod scenario;
|
||||
mod scenario_activation;
|
||||
// Pure saved-geometry bridge; workspace integration is a separate checkpoint.
|
||||
#[allow(dead_code)]
|
||||
mod scenario_layout;
|
||||
pub(crate) mod scenario_layout;
|
||||
mod scenario_lifecycle;
|
||||
pub(crate) mod source_draft;
|
||||
pub(crate) mod state_canvas;
|
||||
|
||||
@@ -6,6 +6,8 @@ use oa_core::{DiagramLayout, ScenarioLayout};
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) mod presentation;
|
||||
|
||||
type Candidate = BTreeMap<Id, f32>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -30,13 +32,13 @@ struct MessageAnchors {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum Diagnostic {
|
||||
pub(crate) enum Diagnostic {
|
||||
SourceOrderCrossing { scope: Id, before: Id, after: Id },
|
||||
FrameExtendsPastFollowingAnchor { frame: Id, following: Id },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) enum GeometryIssue {
|
||||
pub(crate) enum GeometryIssue {
|
||||
UnknownAnchor(Id),
|
||||
LifecycleGeometryNotSupported { lifeline: Id, scope: Id },
|
||||
InvalidRepresentation(Id),
|
||||
@@ -474,7 +476,7 @@ impl Plan {
|
||||
|
||||
/// An appearance identity, never inferred from selection or from the owner alone.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum Representation {
|
||||
pub(crate) enum Representation {
|
||||
Derived { owner: Id },
|
||||
Saved { diagram: Id },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Display policy for one requested Scenario representation, without authoring.
|
||||
//! The resolver remains the sole renderer. Freshness and editor admission belong
|
||||
//! to the caller; a capability here never bypasses those guards.
|
||||
use super::{Diagnostic, GeometryIssue, Representation, ResolvedScenarioPlan, resolve};
|
||||
use crate::behavior::Scene;
|
||||
use iced::Rectangle;
|
||||
use oa_core::{DiagramLayout, Id, Kind, OccurrenceBody, Project, SharedBody};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum PresentationStatus {
|
||||
Ready,
|
||||
Fallback {
|
||||
requested_issues: Vec<GeometryIssue>,
|
||||
},
|
||||
Unavailable {
|
||||
requested_issues: Vec<GeometryIssue>,
|
||||
automatic_issues: Vec<GeometryIssue>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PresentationCapabilities {
|
||||
/// Actual geometry exists for inspection, Fit and geometry-based navigation.
|
||||
pub(crate) geometry_navigation: bool,
|
||||
/// Still subject to document, gesture, editor and view-visit admission.
|
||||
pub(crate) coordinate_input: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct ScenarioPresentation {
|
||||
requested: Representation,
|
||||
owner: Option<Id>,
|
||||
status: PresentationStatus,
|
||||
geometry: Option<ResolvedScenarioPlan>,
|
||||
// Saved metadata must survive a complete automatic fallback.
|
||||
latent_messages: Vec<Id>,
|
||||
}
|
||||
|
||||
impl ScenarioPresentation {
|
||||
pub(crate) fn requested(&self) -> Representation {
|
||||
self.requested
|
||||
}
|
||||
pub(crate) fn owner(&self) -> Option<Id> {
|
||||
self.owner
|
||||
}
|
||||
pub(crate) fn status(&self) -> &PresentationStatus {
|
||||
&self.status
|
||||
}
|
||||
pub(crate) fn geometry_representation(&self) -> Option<Representation> {
|
||||
self.geometry
|
||||
.as_ref()
|
||||
.map(|geometry| geometry.representation)
|
||||
}
|
||||
pub(crate) fn bounds(&self) -> Option<Rectangle> {
|
||||
self.geometry.as_ref().map(|geometry| geometry.bounds)
|
||||
}
|
||||
/// Diagnostics describe the actual displayed geometry. Requested hard failures
|
||||
/// are retained separately in status(), never presented as automatic warnings.
|
||||
pub(crate) fn diagnostics(&self) -> &[Diagnostic] {
|
||||
self.geometry
|
||||
.as_ref()
|
||||
.map_or(&[], |geometry| &geometry.plan.diagnostics)
|
||||
}
|
||||
pub(crate) fn latent_messages(&self) -> &[Id] {
|
||||
&self.latent_messages
|
||||
}
|
||||
pub(crate) fn capabilities(&self) -> PresentationCapabilities {
|
||||
PresentationCapabilities {
|
||||
geometry_navigation: self.geometry.is_some(),
|
||||
coordinate_input: matches!(self.status, PresentationStatus::Ready),
|
||||
}
|
||||
}
|
||||
pub(in crate::behavior) fn scene(&self) -> Option<&Scene> {
|
||||
self.geometry.as_ref().map(|geometry| &geometry.scene)
|
||||
}
|
||||
pub(in crate::behavior) fn into_scene(self) -> Option<Scene> {
|
||||
self.geometry.map(|geometry| geometry.scene)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a structurally accepted Project for passive display. This is not a
|
||||
/// second model validator: identity and resolver-reported structural failures
|
||||
/// cannot be converted into a more permissive automatic representation.
|
||||
pub(crate) fn present(
|
||||
project: &Project,
|
||||
requested: Representation,
|
||||
selected: Id,
|
||||
) -> ScenarioPresentation {
|
||||
let owner = checked_owner(project, requested);
|
||||
let Some(owner_id) = owner else {
|
||||
let id = match requested {
|
||||
Representation::Derived { owner } => owner,
|
||||
Representation::Saved { diagram } => diagram,
|
||||
};
|
||||
return ScenarioPresentation {
|
||||
requested,
|
||||
owner: None,
|
||||
geometry: None,
|
||||
latent_messages: vec![],
|
||||
status: PresentationStatus::Unavailable {
|
||||
requested_issues: vec![GeometryIssue::InvalidRepresentation(id)],
|
||||
automatic_issues: vec![],
|
||||
},
|
||||
};
|
||||
};
|
||||
match resolve(project, requested, selected) {
|
||||
Ok(geometry) => ScenarioPresentation {
|
||||
requested,
|
||||
owner,
|
||||
status: PresentationStatus::Ready,
|
||||
latent_messages: geometry.latent_messages.clone(),
|
||||
geometry: Some(geometry),
|
||||
},
|
||||
Err(requested_issues) => {
|
||||
if matches!(requested, Representation::Derived { .. })
|
||||
|| requested_issues.iter().any(structural_failure)
|
||||
{
|
||||
return ScenarioPresentation {
|
||||
requested,
|
||||
owner,
|
||||
geometry: None,
|
||||
latent_messages: vec![],
|
||||
status: PresentationStatus::Unavailable {
|
||||
requested_issues,
|
||||
automatic_issues: vec![],
|
||||
},
|
||||
};
|
||||
}
|
||||
match resolve(
|
||||
project,
|
||||
Representation::Derived { owner: owner_id },
|
||||
selected,
|
||||
) {
|
||||
Ok(geometry) => {
|
||||
let latent_messages =
|
||||
saved_latent_messages(project, requested, &geometry.scene);
|
||||
ScenarioPresentation {
|
||||
requested,
|
||||
owner,
|
||||
status: PresentationStatus::Fallback { requested_issues },
|
||||
geometry: Some(geometry),
|
||||
latent_messages,
|
||||
}
|
||||
}
|
||||
Err(automatic_issues) => ScenarioPresentation {
|
||||
requested,
|
||||
owner,
|
||||
geometry: None,
|
||||
latent_messages: vec![],
|
||||
status: PresentationStatus::Unavailable {
|
||||
requested_issues,
|
||||
automatic_issues,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_owner(project: &Project, requested: Representation) -> Option<Id> {
|
||||
let owner = match requested {
|
||||
Representation::Derived { owner } => owner,
|
||||
Representation::Saved { diagram } => {
|
||||
let view = project.diagrams.get(&diagram)?;
|
||||
let owner = view.projection.scenario()?;
|
||||
if view.id != diagram || project.elements.get(&owner)?.layer() != view.layer {
|
||||
return None;
|
||||
}
|
||||
owner
|
||||
}
|
||||
};
|
||||
project
|
||||
.elements
|
||||
.get(&owner)
|
||||
.is_some_and(|element| element.kind() == Kind::Scenario)
|
||||
.then_some(owner)
|
||||
}
|
||||
|
||||
fn structural_failure(issue: &GeometryIssue) -> bool {
|
||||
// These violate the existing identity/address/finite-coordinate admission
|
||||
// contract. Geometry support failures below do not invalidate stored intent.
|
||||
match issue {
|
||||
GeometryIssue::InvalidRepresentation(_)
|
||||
| GeometryIssue::UnknownAnchor(_)
|
||||
| GeometryIssue::InvalidCoordinate(_)
|
||||
| GeometryIssue::DuplicateCoordinate(_)
|
||||
| GeometryIssue::ExpandedAddressNotSupported(_)
|
||||
| GeometryIssue::NonFinite(_) => true,
|
||||
GeometryIssue::LifecycleGeometryNotSupported { .. }
|
||||
| GeometryIssue::CoincidentDistinctEndpoints(_)
|
||||
| GeometryIssue::TypedActivationOutsideScope { .. }
|
||||
| GeometryIssue::TypedActivationOutsideLifetime { .. }
|
||||
| GeometryIssue::TypedActivationCrossingNotSupported { .. }
|
||||
| GeometryIssue::CompoundMoveNotImplemented(_)
|
||||
| GeometryIssue::FixedFrameHeaderWouldMove { .. }
|
||||
| GeometryIssue::OperandScopeOutsideEnclosingInterval { .. }
|
||||
| GeometryIssue::LifecycleAnchorOutsideRenderScope { .. }
|
||||
| GeometryIssue::LifetimeInversionNotSupported { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn saved_latent_messages(project: &Project, requested: Representation, scene: &Scene) -> Vec<Id> {
|
||||
let Representation::Saved { diagram } = requested else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(DiagramLayout::Scenario(layout)) = project.diagrams[&diagram].layout.as_ref() else {
|
||||
return vec![];
|
||||
};
|
||||
let lines: BTreeSet<_> = scene.lines.iter().map(|line| line.id).collect();
|
||||
layout
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|message| {
|
||||
let id = message.address.occurrence;
|
||||
(message.address.references.is_empty()
|
||||
&& scene.rows.contains_key(&id)
|
||||
&& !lines.contains(&id)
|
||||
&& matches!(
|
||||
project
|
||||
.elements
|
||||
.get(&id)
|
||||
.and_then(|element| element.shared()),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { .. },
|
||||
..
|
||||
})
|
||||
))
|
||||
.then_some(id)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
use super::*;
|
||||
use crate::document::Document;
|
||||
use oa_core::{
|
||||
Command, Diagram, OccurrenceEndpoint, ProjectionKind, ScenarioLanePosition, ScenarioLayout,
|
||||
ScenarioMessagePosition, language, validation,
|
||||
};
|
||||
|
||||
fn fixture() -> (Project, Id) {
|
||||
let project = language::parse(FIXTURE).unwrap();
|
||||
assert_admitted(&project);
|
||||
let owner = named(&project, "conversation");
|
||||
(project, owner)
|
||||
}
|
||||
fn named(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
fn assert_admitted(project: &Project) {
|
||||
let errors: Vec<_> = validation::validate(project)
|
||||
.into_iter()
|
||||
.filter(|issue| issue.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
}
|
||||
fn save(project: &mut Project, owner: Id, layout: Option<ScenarioLayout>) -> Id {
|
||||
let id = Id::new_v4();
|
||||
project.diagrams.insert(
|
||||
id,
|
||||
Diagram {
|
||||
id,
|
||||
alias: format!("view_{}", project.diagrams.len()),
|
||||
name: "Saved conversation".into(),
|
||||
description: String::new(),
|
||||
layer: project.elements[&owner].layer(),
|
||||
projection: ProjectionKind::Scenario { scenario: owner },
|
||||
view_options: Default::default(),
|
||||
nodes: Default::default(),
|
||||
routes: vec![],
|
||||
layout: layout.map(DiagramLayout::Scenario),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
fn layout(messages: &[(Id, f32)], lanes: &[(Id, f32)]) -> ScenarioLayout {
|
||||
ScenarioLayout {
|
||||
lanes: lanes
|
||||
.iter()
|
||||
.map(|&(lifeline, x)| ScenarioLanePosition { lifeline, x })
|
||||
.collect(),
|
||||
messages: messages
|
||||
.iter()
|
||||
.map(|&(occurrence, send_y)| ScenarioMessagePosition {
|
||||
address: OccurrenceEndpoint {
|
||||
occurrence,
|
||||
references: vec![],
|
||||
},
|
||||
send_y,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
fn same_geometry(a: &Scene, b: &Scene) {
|
||||
assert!(
|
||||
!a.activations.is_empty(),
|
||||
"fallback must include real typed executions"
|
||||
);
|
||||
assert_eq!(a.activations, b.activations);
|
||||
assert_eq!(a.lifecycle, b.lifecycle);
|
||||
assert_eq!(a.rows, b.rows);
|
||||
assert_eq!(a.sequence_bounds, b.sequence_bounds);
|
||||
assert_eq!(a.render_lanes, b.render_lanes);
|
||||
assert_eq!(a.lanes, b.lanes);
|
||||
assert_eq!(a.open_calls, b.open_calls);
|
||||
assert_eq!((a.size, a.layout_origin), (b.size, b.layout_origin));
|
||||
let boxes = |scene: &Scene| {
|
||||
scene
|
||||
.boxes
|
||||
.iter()
|
||||
.map(|item| {
|
||||
(
|
||||
item.id,
|
||||
item.rect,
|
||||
item.title.clone(),
|
||||
item.detail.clone(),
|
||||
item.marker,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let lines = |scene: &Scene| {
|
||||
scene
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
(
|
||||
line.id,
|
||||
line.points.clone(),
|
||||
line.label.clone(),
|
||||
line.label_at,
|
||||
line.dashed,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let slots = |scene: &Scene| {
|
||||
scene
|
||||
.slots
|
||||
.iter()
|
||||
.map(|slot| {
|
||||
(
|
||||
slot.slot.parent,
|
||||
slot.slot.before,
|
||||
slot.y,
|
||||
slot.label.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(boxes(a), boxes(b));
|
||||
assert_eq!(lines(a), lines(b));
|
||||
assert_eq!(slots(a), slots(b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_and_empty_saved_layouts_remain_saved_ready_representations() {
|
||||
let (mut project, owner) = fixture();
|
||||
let absent = save(&mut project, owner, None);
|
||||
let empty = save(&mut project, owner, Some(ScenarioLayout::default()));
|
||||
assert_admitted(&project);
|
||||
let automatic = present(&project, Representation::Derived { owner }, owner);
|
||||
assert_eq!(automatic.status(), &PresentationStatus::Ready);
|
||||
for diagram in [absent, empty] {
|
||||
let requested = Representation::Saved { diagram };
|
||||
let displayed = present(&project, requested, owner);
|
||||
assert_eq!(displayed.requested(), requested);
|
||||
assert_eq!(displayed.geometry_representation(), Some(requested));
|
||||
assert_eq!(displayed.owner(), Some(owner));
|
||||
assert_eq!(displayed.status(), &PresentationStatus::Ready);
|
||||
assert!(displayed.capabilities().coordinate_input);
|
||||
assert_eq!(displayed.bounds(), automatic.bounds());
|
||||
same_geometry(displayed.scene().unwrap(), automatic.scene().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_crossing_diagnostic_keeps_authored_geometry_and_latent_identity() {
|
||||
let (mut project, owner) = fixture();
|
||||
let first = named(&project, "first");
|
||||
let second = named(&project, "second");
|
||||
let left = named(&project, "left");
|
||||
let unfinished = named(&project, "unfinished");
|
||||
let diagram = save(
|
||||
&mut project,
|
||||
owner,
|
||||
Some(layout(
|
||||
&[(first, 1000.), (unfinished, 2000.)],
|
||||
&[(left, 900.)],
|
||||
)),
|
||||
);
|
||||
assert_admitted(&project);
|
||||
let shown = present(&project, Representation::Saved { diagram }, first);
|
||||
assert_eq!(shown.status(), &PresentationStatus::Ready);
|
||||
assert_eq!(shown.scene().unwrap().rows[&first].1, 1000.);
|
||||
assert_eq!(shown.scene().unwrap().rows[&second].1, 240.);
|
||||
assert_eq!(shown.scene().unwrap().render_lanes[&left], 900.);
|
||||
assert!(
|
||||
shown
|
||||
.diagnostics()
|
||||
.contains(&Diagnostic::SourceOrderCrossing {
|
||||
scope: owner,
|
||||
before: first,
|
||||
after: second
|
||||
})
|
||||
);
|
||||
assert_eq!(shown.latent_messages(), &[unfinished]);
|
||||
assert_eq!(shown.scene().unwrap().rows[&unfinished].1, 516.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_failure_uses_complete_automatic_geometry_but_retains_each_saved_request_and_latent_notice()
|
||||
{
|
||||
let (mut project, owner) = fixture();
|
||||
let call = named(&project, "call");
|
||||
let reply = named(&project, "reply");
|
||||
let unfinished = named(&project, "unfinished");
|
||||
let left = named(&project, "left");
|
||||
let right = named(&project, "right");
|
||||
let authored = layout(
|
||||
&[(reply, 300.), (unfinished, 2000.)],
|
||||
&[(left, 900.), (right, -500.)],
|
||||
);
|
||||
let a = save(&mut project, owner, Some(authored.clone()));
|
||||
let b = save(&mut project, owner, Some(authored));
|
||||
assert_admitted(&project);
|
||||
let before = project.clone();
|
||||
let automatic = present(&project, Representation::Derived { owner }, call);
|
||||
let control = automatic.scene().unwrap();
|
||||
assert_eq!(control.render_lanes[&left], 160.);
|
||||
assert_eq!(control.render_lanes[&right], 400.);
|
||||
assert_eq!(control.rows[&reply].1, 424.);
|
||||
assert_eq!(control.activations.len(), 1);
|
||||
assert_eq!(
|
||||
(
|
||||
control.activations[0].rect.y,
|
||||
control.activations[0].rect.height
|
||||
),
|
||||
(356., 68.)
|
||||
);
|
||||
for diagram in [a, b] {
|
||||
let request = Representation::Saved { diagram };
|
||||
let shown = present(&project, request, call);
|
||||
assert_eq!(shown.requested(), request);
|
||||
assert_eq!(
|
||||
shown.geometry_representation(),
|
||||
Some(Representation::Derived { owner })
|
||||
);
|
||||
assert_eq!(
|
||||
shown.status(),
|
||||
&PresentationStatus::Fallback {
|
||||
requested_issues: vec![GeometryIssue::TypedActivationCrossingNotSupported {
|
||||
call,
|
||||
event: reply
|
||||
}],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
shown.capabilities(),
|
||||
PresentationCapabilities {
|
||||
geometry_navigation: true,
|
||||
coordinate_input: false
|
||||
}
|
||||
);
|
||||
assert_eq!(shown.latent_messages(), &[unfinished]);
|
||||
assert!(shown.diagnostics().is_empty());
|
||||
assert_eq!(shown.bounds(), automatic.bounds());
|
||||
same_geometry(shown.scene().unwrap(), control);
|
||||
}
|
||||
assert_eq!(project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_incompatible_identity_and_structural_coordinate_errors_are_unavailable() {
|
||||
let (mut project, owner) = fixture();
|
||||
let left = named(&project, "left");
|
||||
let diagram = save(&mut project, owner, None);
|
||||
let missing = Id::new_v4();
|
||||
for request in [
|
||||
Representation::Saved { diagram: missing },
|
||||
Representation::Derived { owner: left },
|
||||
] {
|
||||
let shown = present(&project, request, owner);
|
||||
assert_eq!(shown.requested(), request);
|
||||
assert_eq!(shown.owner(), None);
|
||||
assert!(
|
||||
matches!(shown.status(), PresentationStatus::Unavailable { automatic_issues, .. } if automatic_issues.is_empty())
|
||||
);
|
||||
assert!(shown.scene().is_none() && shown.bounds().is_none());
|
||||
assert_eq!(
|
||||
shown.capabilities(),
|
||||
PresentationCapabilities {
|
||||
geometry_navigation: false,
|
||||
coordinate_input: false
|
||||
}
|
||||
);
|
||||
}
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection =
|
||||
ProjectionKind::Scenario { scenario: missing };
|
||||
assert!(matches!(
|
||||
present(&project, Representation::Saved { diagram }, owner).status(),
|
||||
PresentationStatus::Unavailable { .. }
|
||||
));
|
||||
// Deliberately invalid-model control, not a warning-admitted geometry failure.
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection =
|
||||
ProjectionKind::Scenario { scenario: owner };
|
||||
project.diagrams.get_mut(&diagram).unwrap().layout = Some(DiagramLayout::Scenario(layout(
|
||||
&[],
|
||||
&[(left, oa_core::MAX_COORDINATE + 1.)],
|
||||
)));
|
||||
assert!(
|
||||
validation::validate(&project)
|
||||
.iter()
|
||||
.any(|issue| issue.severity == validation::Severity::Error)
|
||||
);
|
||||
let before = project.clone();
|
||||
let shown = present(&project, Representation::Saved { diagram }, owner);
|
||||
assert!(
|
||||
matches!(shown.status(), PresentationStatus::Unavailable { requested_issues, automatic_issues }
|
||||
if requested_issues == &[GeometryIssue::InvalidCoordinate(left)] && automatic_issues.is_empty())
|
||||
);
|
||||
assert!(shown.scene().is_none());
|
||||
assert_eq!(project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passive_fallback_preserves_exact_document_source_and_one_step_undo_redo() {
|
||||
let (mut project, owner) = fixture();
|
||||
let first = named(&project, "first");
|
||||
let reply = named(&project, "reply");
|
||||
let view = save(&mut project, owner, Some(layout(&[(reply, 300.)], &[])));
|
||||
assert_admitted(&project);
|
||||
let mut document = Document::from_project(project, Default::default()).unwrap();
|
||||
let initial_project = document.project().clone();
|
||||
let initial_source = document.bundle().clone();
|
||||
document
|
||||
.execute(Command::Rename(first, "An intentional prior edit".into()))
|
||||
.unwrap();
|
||||
let accepted_project = document.project().clone();
|
||||
let accepted_source = document.bundle().clone();
|
||||
let revision = document.revision();
|
||||
{
|
||||
let shown = present(
|
||||
document.project(),
|
||||
Representation::Saved { diagram: view },
|
||||
first,
|
||||
);
|
||||
assert!(matches!(
|
||||
shown.status(),
|
||||
PresentationStatus::Fallback { .. }
|
||||
));
|
||||
assert!(shown.scene().is_some());
|
||||
}
|
||||
assert_eq!(document.project(), &accepted_project);
|
||||
assert_eq!(document.bundle(), &accepted_source);
|
||||
assert_eq!(document.revision(), revision);
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.project(), &initial_project);
|
||||
assert_eq!(document.bundle(), &initial_source);
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), &accepted_project);
|
||||
assert_eq!(document.bundle(), &accepted_source);
|
||||
}
|
||||
|
||||
const FIXTURE: &str = r#"arcadia 7;
|
||||
project display_probe "Scenario display probe" id "70000000-0000-4000-8000-000000000001" {
|
||||
oa_activity source "Source" id "70000000-0000-4000-8000-000000000002" {}
|
||||
oa_activity target "Target" id "70000000-0000-4000-8000-000000000003" {}
|
||||
interaction exchange "Repeated exchange" id "70000000-0000-4000-8000-000000000004" { source @source; target @target; }
|
||||
oa_scenario conversation "Conversation" id "70000000-0000-4000-8000-000000000005" { viewpoint OperationalActivity; steps [@first, @second, @call, @reply, @unfinished]; }
|
||||
oa_lifeline left "Left" id "70000000-0000-4000-8000-000000000006" { parent @conversation; represented @source; }
|
||||
oa_lifeline right "Right" id "70000000-0000-4000-8000-000000000007" { parent @conversation; represented @target; }
|
||||
oa_occurrence first "First" id "70000000-0000-4000-8000-000000000008" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
|
||||
oa_occurrence second "Second" id "70000000-0000-4000-8000-000000000009" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
|
||||
oa_occurrence call "Call" id "70000000-0000-4000-8000-00000000000a" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Call; }; }; }
|
||||
oa_occurrence reply "Reply" id "70000000-0000-4000-8000-00000000000b" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Reply; reply_to @call; }; }; }
|
||||
oa_occurrence unfinished "Unfinished" id "70000000-0000-4000-8000-00000000000c" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver none; message_kind Asynchronous; }; }; }
|
||||
}
|
||||
"#;
|
||||
Reference in New Issue
Block a user