341 lines
13 KiB
Rust
341 lines
13 KiB
Rust
//! App-boundary regressions for the coordinated parent wiring. No I/O task is
|
|
//! polled: file dialogs, checkpoint writes and lesson loading remain inactive.
|
|
use super::*;
|
|
use crate::app::{App, Message, Page, Pending};
|
|
use std::sync::Arc;
|
|
|
|
fn send(app: &mut App, event: Event) {
|
|
let _ = app.update(Message::Behavior(crate::behavior::Event::Creation(event)));
|
|
}
|
|
fn raw_property(app: &App) -> serde_json::Value {
|
|
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap()
|
|
}
|
|
fn begin(app: &mut App, layer: Layer, family: Family, name: &str) {
|
|
let _ = app.update(Message::Layer(layer));
|
|
let _ = app.update(Message::Page(Page::Behavior));
|
|
send(app, Event::Open(layer));
|
|
send(app, Event::Choose(family));
|
|
send(app, Event::Name(name.into()));
|
|
}
|
|
|
|
fn focused_escape() -> Message {
|
|
use iced::advanced::{Layout, Shell, layout, widget};
|
|
use iced::keyboard::{self, key::Named};
|
|
let mut input: iced::Element<'_, (), iced::Theme, ()> = text_input("Name", "Unfinished name")
|
|
.id(NAME_INPUT)
|
|
.on_input(|_| ())
|
|
.into();
|
|
let mut tree = widget::Tree::new(input.as_widget());
|
|
let size = iced::Size::new(300., 40.);
|
|
let node =
|
|
input
|
|
.as_widget_mut()
|
|
.layout(&mut tree, &(), &layout::Limits::new(iced::Size::ZERO, size));
|
|
tree.state
|
|
.downcast_mut::<iced::widget::text_input::State<()>>()
|
|
.focus();
|
|
let event = iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
|
key: keyboard::Key::Named(Named::Escape),
|
|
modified_key: keyboard::Key::Named(Named::Escape),
|
|
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape),
|
|
location: keyboard::Location::Standard,
|
|
modifiers: keyboard::Modifiers::empty(),
|
|
text: None,
|
|
repeat: false,
|
|
});
|
|
let mut messages = Vec::new();
|
|
let mut shell = Shell::new(&mut messages);
|
|
input.as_widget_mut().update(
|
|
&mut tree,
|
|
&event,
|
|
Layout::new(&node),
|
|
iced::mouse::Cursor::Unavailable,
|
|
&(),
|
|
&mut iced::advanced::clipboard::Null,
|
|
&mut shell,
|
|
&iced::Rectangle::new(iced::Point::ORIGIN, size),
|
|
);
|
|
assert!(shell.is_event_captured());
|
|
assert_eq!(
|
|
captured_escape(
|
|
event.clone(),
|
|
iced::event::Status::Ignored,
|
|
iced::window::Id::unique()
|
|
),
|
|
None
|
|
);
|
|
captured_escape(
|
|
event,
|
|
iced::event::Status::Captured,
|
|
iced::window::Id::unique(),
|
|
)
|
|
.unwrap();
|
|
Message::Escape
|
|
}
|
|
|
|
#[test]
|
|
fn app_first_captured_escape_dismisses_file_or_pending_overlay_before_creation() {
|
|
for file in [false, true] {
|
|
let mut app = App::with_project(Project::empty());
|
|
begin(
|
|
&mut app,
|
|
Layer::Oa,
|
|
Family::Sequence,
|
|
"Keep while overlay closes",
|
|
);
|
|
let raw = app.behavior.recovery_snapshot().unwrap();
|
|
let accepted = app.document.snapshot();
|
|
if file {
|
|
app.file_prompt = Some(crate::app::FilePrompt {
|
|
operation: crate::app::FileOperation::Save,
|
|
path: "unsubmitted-name.arcadia".into(),
|
|
overwrite: false,
|
|
});
|
|
} else {
|
|
app.pending = Some(Pending::Walkthrough);
|
|
}
|
|
let _ = app.update(focused_escape());
|
|
assert!(app.file_prompt.is_none());
|
|
assert!(app.pending.is_none());
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
let _ = app.update(focused_escape());
|
|
assert!(!app.behavior.has_pending());
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn app_creation_opens_the_actual_new_owner_instead_of_first_or_previously_active_behavior() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut p = Project::empty();
|
|
let old = p
|
|
.add_element(Family::Sequence.kind(layer), layer, "Old behavior", None)
|
|
.unwrap();
|
|
let mut app = App::with_project(p.clone());
|
|
let _ = app.update(Message::Select(old));
|
|
begin(&mut app, layer, Family::Sequence, " New exact path 雨 ");
|
|
let target = app.behavior.creation.created().unwrap();
|
|
assert_ne!(target.behavior, old);
|
|
let before = app.document.bundle().clone();
|
|
send(&mut app, Event::Submit);
|
|
assert_eq!(app.page, Page::Behavior);
|
|
assert_eq!(app.layer, layer);
|
|
assert_eq!(app.selected, target.behavior);
|
|
assert_eq!(app.behavior.active_owner, Some(target.behavior));
|
|
assert_eq!(crate::behavior::selected_owner(&app), Some(target.behavior));
|
|
assert_eq!(
|
|
app.project().elements[&target.behavior].name,
|
|
" New exact path 雨 "
|
|
);
|
|
assert!(!app.behavior.has_pending());
|
|
let after = app.document.bundle().clone();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
assert!(!app.document.can_undo());
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &after);
|
|
assert_eq!(app.project().elements[&old], p.elements[&old]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn app_machine_behavior_submit_opens_its_opted_in_view_as_one_undoable_transaction() {
|
|
for first_view in [false, true] {
|
|
let mut p = Project::empty();
|
|
let owner = p
|
|
.add_element(Kind::System, Layer::Sa, "Flight system", None)
|
|
.unwrap();
|
|
let mut app = App::with_project(p.clone());
|
|
begin(&mut app, Layer::Sa, Family::Machine, " Flight modes 雨 ");
|
|
send(&mut app, Event::Owner(owner));
|
|
send(&mut app, Event::FirstView(first_view));
|
|
let target = app.behavior.creation.created().unwrap();
|
|
let before = app.document.bundle().clone();
|
|
let _ = app.update(Message::Behavior(crate::behavior::Event::Submit));
|
|
assert!(!app.behavior.has_pending(), "{}", app.status);
|
|
assert_eq!(app.selected, target.behavior);
|
|
assert_eq!(app.behavior.active_owner, Some(target.behavior));
|
|
assert_eq!(app.behavior.diagram.diagram, target.diagram);
|
|
assert_eq!(
|
|
crate::behavior::state_view::diagram(&app, target.behavior),
|
|
target.diagram
|
|
);
|
|
assert_eq!(app.project().elements[&target.behavior].parent, Some(owner));
|
|
assert_eq!(app.page, Page::Behavior);
|
|
let after = app.document.bundle().clone();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
assert_eq!(app.project(), &p);
|
|
assert!(!app.document.can_undo());
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &after);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn app_stale_layer_open_never_starts_or_replaces_a_draft() {
|
|
let mut app = App::with_project(Project::empty());
|
|
let _ = app.update(Message::Layer(Layer::Sa));
|
|
app.page = Page::Behavior;
|
|
let accepted = app.document.snapshot();
|
|
send(&mut app, Event::Open(Layer::Oa));
|
|
assert!(!app.behavior.has_pending());
|
|
assert_eq!(app.layer, Layer::Sa);
|
|
send(&mut app, Event::Open(Layer::Sa));
|
|
send(&mut app, Event::Choose(Family::Sequence));
|
|
send(&mut app, Event::Name("Retained system chain".into()));
|
|
let raw = app.behavior.recovery_snapshot().unwrap();
|
|
send(&mut app, Event::Open(Layer::Oa));
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
}
|
|
|
|
#[test]
|
|
fn app_new_never_auto_applies_valid_or_invalid_property_and_source_drafts() {
|
|
for dirty in [
|
|
"property-valid",
|
|
"property-invalid",
|
|
"source-valid",
|
|
"source-invalid",
|
|
] {
|
|
let mut app = App::with_project(Project::empty());
|
|
app.page = Page::Behavior;
|
|
match dirty {
|
|
"property-valid" => app
|
|
.draft
|
|
.as_mut()
|
|
.unwrap()
|
|
.input("/name", "Raw property name".into()),
|
|
"property-invalid" => app.draft.as_mut().unwrap().input("/id", "-".into()),
|
|
"source-valid" => app.source_workspace.edit(
|
|
format!("{}\n// Retained comment\n", app.source_workspace.text()),
|
|
app.source_editor.cursor(),
|
|
),
|
|
_ => app
|
|
.source_workspace
|
|
.edit("unfinished source 雨".into(), app.source_editor.cursor()),
|
|
}
|
|
let accepted = app.document.snapshot();
|
|
let property = raw_property(&app);
|
|
let workspace = app.source_workspace.recovery_snapshot();
|
|
send(&mut app, Event::Open(Layer::Oa));
|
|
assert!(
|
|
!app.behavior.creation.has_pending(),
|
|
"{dirty}: New must refuse without applying another editor"
|
|
);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
assert_eq!(raw_property(&app), property);
|
|
assert_eq!(
|
|
serde_json::to_value(app.source_workspace.recovery_snapshot()).unwrap(),
|
|
serde_json::to_value(workspace).unwrap()
|
|
);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn app_choosing_another_behavior_or_layer_cannot_commit_a_creation_draft() {
|
|
let mut p = Project::empty();
|
|
let old = p
|
|
.add_element(Kind::Process, Layer::Oa, "Old process", None)
|
|
.unwrap();
|
|
let mut app = App::with_project(p);
|
|
begin(&mut app, Layer::Oa, Family::Sequence, "Retain this process");
|
|
let raw = app.behavior.recovery_snapshot().unwrap();
|
|
let accepted = app.document.snapshot();
|
|
let _ = app.update(Message::Layer(Layer::Sa));
|
|
let _ = app.update(Message::BehaviorOwner(crate::app::Choice {
|
|
id: old,
|
|
label: "Old process".into(),
|
|
}));
|
|
assert_eq!(app.layer, Layer::Oa);
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
let _ = app.update(Message::Escape);
|
|
assert!(!app.behavior.has_pending());
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn app_lesson_request_cancel_retains_creation_then_escape_discards_only_the_draft() {
|
|
let mut app = App::with_project(Project::empty());
|
|
begin(
|
|
&mut app,
|
|
Layer::Oa,
|
|
Family::Sequence,
|
|
"Unfinished engineering path",
|
|
);
|
|
let raw = app.behavior.recovery_snapshot().unwrap();
|
|
let accepted = app.document.snapshot();
|
|
let task = app.update(Message::Home(crate::home::Event::Walkthrough));
|
|
assert!(matches!(app.pending, Some(Pending::Walkthrough)));
|
|
assert!(iced_runtime::task::into_stream(task).is_none());
|
|
let _ = app.update(Message::Escape);
|
|
assert!(app.pending.is_none());
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
let _ = app.update(Message::Escape);
|
|
assert!(!app.behavior.has_pending());
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
}
|
|
|
|
#[test]
|
|
fn app_recovered_rootless_sa_creation_retains_layer_ids_and_other_raw_drafts() {
|
|
let mut app = App::with_project(Project::empty());
|
|
begin(
|
|
&mut app,
|
|
Layer::Sa,
|
|
Family::Scenario,
|
|
" Recovered system scenario 雨 ",
|
|
);
|
|
send(
|
|
&mut app,
|
|
Event::Viewpoint(ScenarioViewpoint::BoundaryInterface),
|
|
);
|
|
// Recovery can contain simultaneous drafts even though new input is guarded.
|
|
app.draft
|
|
.as_mut()
|
|
.unwrap()
|
|
.input("/name", "Retained property".into());
|
|
app.source_workspace.edit(
|
|
"unfinished source preserved".into(),
|
|
app.source_editor.cursor(),
|
|
);
|
|
let snapshot = crate::recovery::Snapshot::capture(
|
|
app.canvas_document,
|
|
&app.document,
|
|
&app.source_workspace,
|
|
app.draft.as_ref(),
|
|
&app.behavior,
|
|
None,
|
|
app.selected,
|
|
)
|
|
.unwrap();
|
|
let expected = snapshot.clone().restore().unwrap();
|
|
let raw = app.behavior.recovery_snapshot().unwrap();
|
|
let mut restored = App::with_project(Project::empty());
|
|
let _ = restored.update(Message::CheckpointLoaded(Ok(Arc::new(snapshot))));
|
|
assert_eq!(restored.page, Page::Behavior);
|
|
assert_eq!(restored.layer, Layer::Sa);
|
|
assert_eq!(restored.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert_eq!(
|
|
restored.behavior.creation.created(),
|
|
app.behavior.creation.created()
|
|
);
|
|
assert_eq!(restored.project(), expected.document.project());
|
|
assert_eq!(restored.document.bundle(), expected.document.bundle());
|
|
assert_eq!(
|
|
restored.source_workspace.bundle(),
|
|
expected.workspace.bundle()
|
|
);
|
|
assert_eq!(raw_property(&restored), raw_property(&app));
|
|
let before = restored.document.snapshot();
|
|
send(&mut restored, Event::Submit);
|
|
assert!(Arc::ptr_eq(&before, &restored.document.snapshot()));
|
|
assert_eq!(restored.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert_eq!(raw_property(&restored), raw_property(&app));
|
|
}
|