Add named behavior creation and verified textual authoring examples
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
//! 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));
|
||||
}
|
||||
Reference in New Issue
Block a user