Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
671 lines
28 KiB
Rust
671 lines
28 KiB
Rust
use super::*;
|
|
use crate::behavior::topology::{self, CanvasEvent};
|
|
impl App {
|
|
pub(crate) fn topology_context(&self) -> Option<topology::Context> {
|
|
let owner = crate::behavior::selected_owner(self)?;
|
|
(self.page == Page::Behavior
|
|
&& matches!(
|
|
self.project().elements.get(&owner)?.kind(),
|
|
Kind::Process | Kind::FunctionalChain
|
|
))
|
|
.then_some(topology::Context {
|
|
document: self.canvas_document,
|
|
owner,
|
|
revision: self.document.revision(),
|
|
epoch: self.canvas_epoch,
|
|
})
|
|
}
|
|
pub(super) fn reveal_process_selection(&self) -> Task<Message> {
|
|
let Some(context) = self.topology_context() else {
|
|
return Task::none();
|
|
};
|
|
let Some(offset) =
|
|
self.behavior
|
|
.topology
|
|
.selection_offset(self.project(), context.owner, self.selected)
|
|
else {
|
|
return Task::none();
|
|
};
|
|
iced::widget::operation::scroll_to("process-canvas", offset)
|
|
}
|
|
pub(super) fn edit_process_canvas(&mut self, event: CanvasEvent) -> Task<Message> {
|
|
let context = event.context();
|
|
if let CanvasEvent::Scrolled(_, area, offset) = event {
|
|
if self.topology_context().is_some_and(|c| {
|
|
c.document == context.document
|
|
&& c.owner == context.owner
|
|
&& c.revision == context.revision
|
|
}) && self.behavior.topology.scrolled(area, offset)
|
|
{
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
return Task::none();
|
|
}
|
|
if let CanvasEvent::Viewport(_, size) = event {
|
|
if self.topology_context().is_some_and(|c| {
|
|
c.document == context.document
|
|
&& c.owner == context.owner
|
|
&& c.revision == context.revision
|
|
}) && self.behavior.topology.viewport(size)
|
|
{
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
return Task::none();
|
|
}
|
|
if self.topology_context() != Some(context) {
|
|
return Task::none();
|
|
}
|
|
self.behavior
|
|
.topology
|
|
.enter(context.document, context.owner);
|
|
match event {
|
|
CanvasEvent::ExpandQualified(_, endpoint) => {
|
|
let mut state = std::mem::take(&mut self.behavior.topology);
|
|
let changed = state.expand_context(self.project(), context.owner, endpoint);
|
|
self.behavior.topology = state;
|
|
if changed {
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
}
|
|
CanvasEvent::OpenQualified(_, endpoint) => {
|
|
if self.behavior.has_pending() || !self.apply() {
|
|
return Task::none();
|
|
}
|
|
let resolved = oa_core::topology_context::TopologyIndex::new(self.project())
|
|
.resolve(context.owner, &endpoint);
|
|
if let Ok(resolved) = resolved {
|
|
self.behavior.topology.remember(
|
|
context.owner,
|
|
endpoint
|
|
.references
|
|
.first()
|
|
.copied()
|
|
.unwrap_or(endpoint.occurrence),
|
|
);
|
|
self.behavior.active_owner = Some(resolved.defining_sequence);
|
|
self.select(endpoint.occurrence);
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
}
|
|
CanvasEvent::SelectQualified(_, endpoint) => {
|
|
if !self.behavior.has_pending()
|
|
&& !self.draft_dirty()
|
|
&& !self.source_dirty()
|
|
&& let Ok(resolved) =
|
|
oa_core::topology_context::TopologyIndex::new(self.project())
|
|
.resolve(context.owner, &endpoint)
|
|
{
|
|
let reference = endpoint
|
|
.references
|
|
.first()
|
|
.copied()
|
|
.unwrap_or(endpoint.occurrence);
|
|
self.select(reference);
|
|
self.behavior.topology.select_context(resolved.address);
|
|
}
|
|
}
|
|
CanvasEvent::Focus(_) => {
|
|
self.behavior.topology.focused = !self.behavior.topology.focused;
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
CanvasEvent::Select(_, id) => {
|
|
if !self.behavior.has_pending()
|
|
&& crate::behavior::selected_behavior(self.project(), id) == Some(context.owner)
|
|
{
|
|
self.select(id);
|
|
self.selected_node = None;
|
|
self.behavior.topology.clear_context();
|
|
}
|
|
}
|
|
CanvasEvent::Choose(_, tool) => {
|
|
if self.behavior.has_pending() || !self.apply() {
|
|
return Task::none();
|
|
}
|
|
self.behavior.topology.choose(tool);
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
CanvasEvent::Zoom(_, zoom) => {
|
|
self.behavior.topology.zoom(zoom);
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
CanvasEvent::Expand(_, id) => {
|
|
let mut state = std::mem::take(&mut self.behavior.topology);
|
|
let changed = state.expand(self.project(), context.owner, id);
|
|
self.behavior.topology = state;
|
|
if changed {
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
}
|
|
CanvasEvent::Open(_, occurrence) => {
|
|
if self.behavior.has_pending() || !self.apply() {
|
|
return Task::none();
|
|
}
|
|
if let Some(target) =
|
|
topology::referenced(self.project(), context.owner, occurrence)
|
|
{
|
|
if self
|
|
.project()
|
|
.elements
|
|
.get(&target)
|
|
.is_some_and(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain))
|
|
{
|
|
self.behavior.topology.remember(context.owner, occurrence);
|
|
self.behavior.active_owner = Some(target);
|
|
}
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
self.select(target);
|
|
}
|
|
}
|
|
CanvasEvent::Back(_) => {
|
|
if self.behavior.has_pending() || !self.apply() {
|
|
return Task::none();
|
|
}
|
|
let mut state = std::mem::take(&mut self.behavior.topology);
|
|
let back = state.back(self.project());
|
|
self.behavior.topology = state;
|
|
if let Some((owner, occurrence)) = back {
|
|
self.behavior.active_owner = Some(owner);
|
|
self.select(occurrence);
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
}
|
|
mutation => {
|
|
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() {
|
|
self.status =
|
|
"Apply or cancel pending edits before changing the process.".into();
|
|
return Task::none();
|
|
}
|
|
match mutation {
|
|
CanvasEvent::Begin {
|
|
tool,
|
|
control,
|
|
insertion,
|
|
..
|
|
} => {
|
|
let mut state = std::mem::take(&mut self.behavior.topology);
|
|
let result = state.begin(self.project(), context, tool, control, insertion);
|
|
self.behavior.topology = state;
|
|
match result {
|
|
Ok(()) => {
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
self.status="Choose the contextual contribution and apply at the captured position.".into();
|
|
}
|
|
Err(e) => self.status = e,
|
|
}
|
|
}
|
|
CanvasEvent::AppendExchange(_) => {
|
|
let mut state = std::mem::take(&mut self.behavior.topology);
|
|
let result = state.append_exchange(self.project(), context.owner);
|
|
self.behavior.topology = state;
|
|
match result {
|
|
Ok(()) => {
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
self.status = "Choose the next exchange; Create appends it with an explicit sequence link.".into();
|
|
}
|
|
Err(e) => self.status = e,
|
|
}
|
|
}
|
|
CanvasEvent::Connect { source, target, .. } => {
|
|
let mut state = std::mem::take(&mut self.behavior.topology);
|
|
let result = state.connect(self.project(), context.owner, source, target);
|
|
self.behavior.topology = state;
|
|
match result {
|
|
Ok(()) => {
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
self.status = "Describe the path condition, then Create.".into();
|
|
}
|
|
Err(e) => self.status = e,
|
|
}
|
|
}
|
|
CanvasEvent::Reconnect {
|
|
link, end, target, ..
|
|
} => {
|
|
match topology::reconnect(self.project(), context.owner, link, end, target)
|
|
{
|
|
Ok(command) => {
|
|
if self.execute(command) {
|
|
self.selected = link;
|
|
self.refresh_draft();
|
|
return self.reveal_process_selection();
|
|
}
|
|
}
|
|
Err(e) => self.status = e,
|
|
}
|
|
}
|
|
CanvasEvent::Reorder {
|
|
occurrence, before, ..
|
|
} => {
|
|
match topology::reorder(self.project(), context.owner, occurrence, before) {
|
|
Ok(command) => {
|
|
if self.execute(command) {
|
|
self.selected = occurrence;
|
|
self.refresh_draft();
|
|
return self.reveal_process_selection();
|
|
}
|
|
}
|
|
Err(e) => self.status = e,
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
Task::none()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::behavior::{
|
|
self,
|
|
topology::{
|
|
CanvasTool, Event, Insertion, Tool,
|
|
direct_tests::{self, Fixture},
|
|
},
|
|
};
|
|
fn setup() -> (App, Fixture) {
|
|
let f = direct_tests::fixture(Layer::Oa);
|
|
let mut app = App::with_project(f.p.clone());
|
|
let _ = app.update(Message::Select(f.owner));
|
|
let _ = app.update(Message::Page(Page::Behavior));
|
|
assert_eq!(app.topology_context().unwrap().owner, f.owner);
|
|
(app, f)
|
|
}
|
|
fn form(app: &mut App, event: Event) {
|
|
let _ = app.update(Message::Behavior(behavior::Event::Topology(event)));
|
|
}
|
|
fn begin(app: &App) -> CanvasEvent {
|
|
CanvasEvent::Begin {
|
|
context: app.topology_context().unwrap(),
|
|
tool: Tool::Work,
|
|
control: ControlKind::Choice,
|
|
insertion: Insertion::default(),
|
|
}
|
|
}
|
|
#[test]
|
|
fn process_focus_retains_rejected_draft_and_guards_selection_until_panels_restore() {
|
|
let (mut app, f) = setup();
|
|
let original = app.document.snapshot();
|
|
let initial = app.selected;
|
|
let event = begin(&app);
|
|
let _ = app.update(Message::TopologyCanvas(event));
|
|
form(
|
|
&mut app,
|
|
Event::Name(" unfinished contextual work ".into()),
|
|
);
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Focus(context)));
|
|
assert!(app.behavior.topology.focused);
|
|
let raw = app.behavior.recovery_snapshot().unwrap();
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Select(
|
|
context, f.nodes[1],
|
|
)));
|
|
assert_eq!(app.selected, initial);
|
|
form(&mut app, Event::Submit);
|
|
assert!(app.behavior.has_pending());
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert!(Arc::ptr_eq(&original, &app.document.snapshot()));
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Focus(context)));
|
|
assert!(!app.behavior.topology.focused);
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
|
|
}
|
|
#[test]
|
|
fn direct_process_append_empty_first_second_exchange_has_exact_source_history() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let (f, next) = direct_tests::empty_exchange_fixture(layer);
|
|
let mut app = App::with_project(f.p.clone());
|
|
let _ = app.update(Message::Select(f.owner));
|
|
let _ = app.update(Message::Page(Page::Behavior));
|
|
let empty = app.document.snapshot();
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Begin {
|
|
context,
|
|
tool: Tool::Exchange,
|
|
control: ControlKind::Choice,
|
|
insertion: Insertion::default(),
|
|
}));
|
|
assert!(app.behavior.has_pending());
|
|
form(
|
|
&mut app,
|
|
Event::Target(Choice {
|
|
id: f.exchange,
|
|
label: "Assessment".into(),
|
|
}),
|
|
);
|
|
form(&mut app, Event::Name("First contribution".into()));
|
|
form(&mut app, Event::Submit);
|
|
assert!(!app.behavior.has_pending(), "{}", app.status);
|
|
let first_id = app.selected;
|
|
let first = app.document.snapshot();
|
|
assert_eq!(
|
|
app.project().elements[&f.owner].steps().unwrap(),
|
|
&[first_id]
|
|
);
|
|
assert_eq!(app.project().relations, empty.decoded.project.relations);
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::AppendExchange(
|
|
context,
|
|
)));
|
|
assert!(
|
|
app.behavior.has_pending(),
|
|
"reachable single-exchange continuation: {}",
|
|
app.status
|
|
);
|
|
assert!(Arc::ptr_eq(&first, &app.document.snapshot()));
|
|
form(
|
|
&mut app,
|
|
Event::Target(Choice {
|
|
id: next,
|
|
label: "Response".into(),
|
|
}),
|
|
);
|
|
form(&mut app, Event::Name("Second contribution".into()));
|
|
form(&mut app, Event::Submit);
|
|
assert!(!app.behavior.has_pending(), "{}", app.status);
|
|
let second_id = app.selected;
|
|
let second = app.document.snapshot();
|
|
assert_ne!(first_id, second_id);
|
|
assert_eq!(
|
|
app.project().elements[&f.owner].steps().unwrap(),
|
|
&[first_id, second_id]
|
|
);
|
|
let links = app
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::SequenceLink)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(links.len(), 1);
|
|
assert_eq!(
|
|
(links[0].source(), links[0].target()),
|
|
(first_id, second_id)
|
|
);
|
|
assert_eq!(
|
|
app.project().elements.len(),
|
|
empty.decoded.project.elements.len() + 2
|
|
);
|
|
for (id, relation) in &empty.decoded.project.relations {
|
|
assert_eq!(app.project().relations[id], *relation);
|
|
}
|
|
for work in f.work {
|
|
assert_eq!(
|
|
app.project().elements[&work],
|
|
empty.decoded.project.elements[&work]
|
|
);
|
|
}
|
|
let reopened = Document::from_bundle(
|
|
second.bundle.clone(),
|
|
oa_core::source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(reopened.project(), app.project());
|
|
assert_eq!(reopened.bundle(), &second.bundle);
|
|
for expected in [&first, &empty] {
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &expected.bundle);
|
|
assert_eq!(app.project(), &expected.decoded.project);
|
|
}
|
|
assert!(
|
|
!app.document.can_undo(),
|
|
"each Create must add exactly one history entry"
|
|
);
|
|
for expected in [&first, &second] {
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &expected.bundle);
|
|
assert_eq!(app.project(), &expected.decoded.project);
|
|
}
|
|
assert!(!app.document.can_redo());
|
|
}
|
|
}
|
|
#[test]
|
|
fn direct_process_app_stale_context_dirty_source_and_properties_never_mutate() {
|
|
let (mut app, _) = setup();
|
|
let snapshot = app.document.snapshot();
|
|
for field in 0..4 {
|
|
let mut event = begin(&app);
|
|
let CanvasEvent::Begin { context, .. } = &mut event else {
|
|
unreachable!()
|
|
};
|
|
match field {
|
|
0 => context.document = Id::new_v4(),
|
|
1 => context.owner = Id::new_v4(),
|
|
2 => context.revision += 1,
|
|
_ => context.epoch += 1,
|
|
};
|
|
let _ = app.update(Message::TopologyCanvas(event));
|
|
assert!(!app.behavior.has_pending());
|
|
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
|
}
|
|
let _ = app.update(Message::Field("/name".into(), "Pending owner name".into()));
|
|
let event = begin(&app);
|
|
let _ = app.update(Message::TopologyCanvas(event));
|
|
assert!(!app.behavior.has_pending());
|
|
assert!(app.draft_dirty());
|
|
let context = app.topology_context().unwrap();
|
|
for event in [
|
|
CanvasEvent::Zoom(context, Some(0.7)),
|
|
CanvasEvent::Viewport(context, iced::Size::new(510., 360.)),
|
|
] {
|
|
let _ = app.update(Message::TopologyCanvas(event));
|
|
assert!(app.draft_dirty());
|
|
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
|
}
|
|
}
|
|
#[test]
|
|
fn direct_process_app_source_failure_retains_form_then_commits_once_and_selects_occurrence() {
|
|
let (mut app, f) = setup();
|
|
let bundle = app.document.bundle().clone();
|
|
let mut limits = oa_core::source_project::ProjectLimits::default();
|
|
limits.model.max_source_bytes =
|
|
bundle.files.values().map(String::len).sum::<usize>() + 3000;
|
|
app.document = Document::from_bundle(bundle.clone(), limits).unwrap();
|
|
app.sync();
|
|
let event = begin(&app);
|
|
let _ = app.update(Message::TopologyCanvas(event));
|
|
form(
|
|
&mut app,
|
|
Event::Target(Choice {
|
|
id: f.work[0],
|
|
label: "Assess".into(),
|
|
}),
|
|
);
|
|
form(&mut app, Event::Name("Retain this long input".repeat(1000)));
|
|
let _ = app.update(Message::Page(Page::Catalog));
|
|
assert_eq!(app.page, Page::Behavior);
|
|
form(&mut app, Event::Submit);
|
|
assert!(app.behavior.has_pending());
|
|
assert_eq!(app.document.bundle(), &bundle);
|
|
assert!(!app.document.can_undo());
|
|
form(&mut app, Event::Name("Recovered contribution".into()));
|
|
form(&mut app, Event::Submit);
|
|
assert!(!app.behavior.has_pending());
|
|
assert_ne!(app.selected, f.owner);
|
|
assert_eq!(app.project().named(app.selected), "Recovered contribution");
|
|
let created = app.selected;
|
|
let after = app.document.bundle().clone();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &bundle);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &after);
|
|
assert!(app.project().contains(created));
|
|
}
|
|
#[test]
|
|
fn direct_process_app_same_owner_selection_preserves_tool_but_owner_change_resets_it() {
|
|
let (mut app, f) = setup();
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Choose(
|
|
context,
|
|
CanvasTool::Connect,
|
|
)));
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Select(
|
|
context, f.nodes[0],
|
|
)));
|
|
assert_eq!(app.topology_context(), Some(context));
|
|
let other = app
|
|
.project()
|
|
.make_element(Kind::Process, Layer::Oa, "Other", None)
|
|
.unwrap();
|
|
let other_id = other.id;
|
|
assert!(app.execute(Command::CreateElement(other)));
|
|
let _ = app.update(Message::BehaviorOwner(Choice {
|
|
id: other_id,
|
|
label: "Other".into(),
|
|
}));
|
|
assert_eq!(app.topology_context().unwrap().owner, other_id);
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Connect {
|
|
context,
|
|
source: f.nodes[0],
|
|
target: f.nodes[2],
|
|
}));
|
|
assert!(!app.behavior.has_pending());
|
|
}
|
|
#[test]
|
|
fn direct_process_app_save_scroll_resize_and_passive_geometry_keep_drafts_and_current_origin() {
|
|
let (mut app, f) = setup();
|
|
let context = app.topology_context().unwrap();
|
|
let original = app.document.snapshot();
|
|
app.busy = true;
|
|
for (area, offset) in [
|
|
(
|
|
behavior::scenario::ScrollArea::Canvas,
|
|
iced::Vector::new(25., 100.),
|
|
),
|
|
(
|
|
behavior::scenario::ScrollArea::Page,
|
|
iced::Vector::new(0., 40.),
|
|
),
|
|
] {
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
|
|
context, area, offset,
|
|
)));
|
|
}
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Viewport(
|
|
context,
|
|
iced::Size::new(500., 300.),
|
|
)));
|
|
assert_eq!(
|
|
app.behavior.topology.scroll_offset(),
|
|
iced::Vector::new(25., 140.)
|
|
);
|
|
assert_ne!(app.topology_context(), Some(context));
|
|
assert!(app.busy);
|
|
let mut stale = context;
|
|
stale.document = Id::new_v4();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
|
|
stale,
|
|
behavior::scenario::ScrollArea::Canvas,
|
|
iced::Vector::new(0., 900.),
|
|
)));
|
|
assert_eq!(
|
|
app.behavior.topology.scroll_offset(),
|
|
iced::Vector::new(25., 140.)
|
|
);
|
|
let root = PathBuf::from("process-scroll-save-fixture");
|
|
let receipt = FileSnapshot {
|
|
document: context.document,
|
|
revision: context.revision,
|
|
snapshot: original.clone(),
|
|
path: root.join(&original.bundle.entry),
|
|
disk: Some(source_files::DiskProject {
|
|
root,
|
|
bundle: original.bundle.clone(),
|
|
expectations: Default::default(),
|
|
reachable: original.bundle.files.keys().cloned().collect(),
|
|
diagnostics: vec![],
|
|
}),
|
|
};
|
|
let _ = app.update(Message::Saved(Ok(Some(receipt))));
|
|
assert!(!app.busy);
|
|
assert!(!app.document.can_undo());
|
|
assert_eq!(app.project(), &f.p);
|
|
let context = app.topology_context().unwrap();
|
|
let size = app.window_size;
|
|
let _ = app.update(Message::WindowResized(size));
|
|
assert_eq!(app.topology_context(), Some(context));
|
|
let _ = app.update(Message::WindowResized(iced::Size::new(
|
|
size.width + 20.,
|
|
size.height,
|
|
)));
|
|
assert_ne!(app.topology_context(), Some(context));
|
|
let event = begin(&app);
|
|
let _ = app.update(Message::TopologyCanvas(event));
|
|
form(&mut app, Event::Name("Keep this form".into()));
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
|
|
context,
|
|
behavior::scenario::ScrollArea::Page,
|
|
iced::Vector::new(0., 80.),
|
|
)));
|
|
assert!(app.behavior.has_pending());
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
}
|
|
#[test]
|
|
fn direct_process_app_open_source_and_back_preserve_local_reference_identity() {
|
|
let (mut app, f) = setup();
|
|
let source = app
|
|
.project()
|
|
.make_element(Kind::Process, Layer::Oa, "Reusable source", None)
|
|
.unwrap();
|
|
let source_id = source.id;
|
|
assert!(app.execute(Command::CreateElement(source)));
|
|
let mut occurrence = app
|
|
.project()
|
|
.make_element(Kind::Occurrence, Layer::Oa, "Local use", Some(f.owner))
|
|
.unwrap();
|
|
let occurrence_id = occurrence.id;
|
|
if let Some(SharedBody::Occurrence { body, .. }) = occurrence.shared_mut() {
|
|
*body = OccurrenceBody::SequenceReference {
|
|
sequence: Some(source_id),
|
|
};
|
|
}
|
|
assert!(app.execute(Command::CreateElement(occurrence)));
|
|
let snapshot = app.document.snapshot();
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Open(
|
|
context,
|
|
occurrence_id,
|
|
)));
|
|
assert_eq!(app.selected, source_id);
|
|
assert_eq!(app.topology_context().unwrap().owner, source_id);
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Back(context)));
|
|
assert_eq!(app.selected, occurrence_id);
|
|
assert_eq!(app.topology_context().unwrap().owner, f.owner);
|
|
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[test]
|
|
fn direct_process_dirty_source_blocks_canvas_creation_without_losing_text() {
|
|
let f = topology::direct_tests::fixture(Layer::Oa);
|
|
let mut app = App::with_project(f.p.clone());
|
|
let _ = app.update(Message::Select(f.owner));
|
|
let _ = app.update(Message::Page(Page::Behavior));
|
|
let original = app.document.snapshot();
|
|
let edited = format!(
|
|
"{}\n// Retained source draft\n",
|
|
app.source_workspace.text()
|
|
);
|
|
let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll));
|
|
let _ = app.update(Message::SourceEdit(text_editor::Action::Edit(
|
|
text_editor::Edit::Paste(edited.clone().into()),
|
|
)));
|
|
assert!(app.source_dirty());
|
|
let context = app.topology_context().unwrap();
|
|
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Begin {
|
|
context,
|
|
tool: topology::Tool::Work,
|
|
control: ControlKind::Choice,
|
|
insertion: topology::Insertion::default(),
|
|
}));
|
|
assert!(!app.behavior.has_pending());
|
|
assert!(app.source_dirty());
|
|
assert_eq!(app.source_workspace.text(), edited);
|
|
assert!(Arc::ptr_eq(&original, &app.document.snapshot()));
|
|
}
|