Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/creation/app_tests.rs
T

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));
}