Files
OneCAD-Draft/src/app/block_editor_tests.rs
T

368 lines
12 KiB
Rust

use super::{Message, ModalKind, OpenCADStudio};
use codec::entities::{Insert, Line};
use codec::types::Vector3;
use codec::{EntityType, Handle};
fn fixture() -> (OpenCADStudio, Handle) {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
app.tabs[i].scene.define_block_raw(
"EDIT_ME",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
))],
);
let reference = app.tabs[i]
.scene
.document
.add_entity(EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO)))
.unwrap();
app.tabs[i].dirty = false;
(app, reference)
}
fn begin(app: &mut OpenCADStudio, reference: Handle) {
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
}
fn circle(app: &mut OpenCADStudio, x: f64) {
let result = app.automation_op(&format!(r#"{{"op":"run","cmd":"CIRCLE {x},0 2"}}"#));
assert_eq!(result["ok"], true, "{result}");
app.finish_pending_history(app.active_tab);
}
fn members(app: &OpenCADStudio) -> Vec<Handle> {
app.tabs[app.active_tab]
.scene
.document
.block_records
.get("EDIT_ME")
.unwrap()
.entity_handles
.clone()
}
fn main_depth(app: &OpenCADStudio) -> usize {
let tab = &app.tabs[app.active_tab];
tab.block_edit_history
.as_ref()
.map(|h| h.main.undo_stack.len())
.unwrap_or(tab.history.undo_stack.len())
}
#[test]
fn session_undo_preserves_membership_and_cannot_reach_drawing_history() {
let (mut app, reference) = fixture();
circle(&mut app, 50.0);
let depth = main_depth(&app);
begin(&mut app, reference);
assert_eq!(main_depth(&app), depth);
circle(&mut app, 1.0);
assert_eq!(members(&app).len(), 2);
app.undo_steps(1);
assert_eq!(members(&app).len(), 1);
assert!(!app.block_edit_dirty(app.active_tab));
app.undo_steps(100);
assert_eq!(main_depth(&app), depth);
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
app.redo_steps(1);
assert_eq!(members(&app).len(), 2);
assert!(members(&app).iter().all(|h| app.tabs[app.active_tab]
.scene
.document
.get_entity(*h)
.is_some()));
}
#[test]
fn repeated_saves_stay_open_and_commit_one_drawing_step_each() {
let (mut app, reference) = fixture();
begin(&mut app, reference);
circle(&mut app, 1.0);
circle(&mut app, 2.0);
let _ = app.dispatch_command("BSAVE");
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
assert_eq!(main_depth(&app), 1);
assert!(!app.block_edit_dirty(app.active_tab));
let _ = app.dispatch_command("BSAVE");
assert_eq!(main_depth(&app), 1, "a clean save is not a new edit");
circle(&mut app, 3.0);
let _ = app.dispatch_command("BSAVE");
assert_eq!(main_depth(&app), 2);
circle(&mut app, 4.0);
let _ = app.dispatch_command("BEDIT_DISCARD");
assert_eq!(members(&app).len(), 4, "discard restores the last save");
assert_eq!(main_depth(&app), 2);
assert_eq!(
app.tabs[app.active_tab]
.history
.undo_stack
.last()
.unwrap()
.label(),
"Edit block EDIT_ME"
);
app.undo_steps(1);
assert_eq!(members(&app).len(), 3);
app.undo_steps(1);
assert_eq!(members(&app).len(), 1);
app.redo_steps(2);
assert_eq!(members(&app).len(), 4);
}
#[test]
fn discard_preserves_drawing_redo_and_never_leaks_local_edits() {
let (mut app, reference) = fixture();
circle(&mut app, 50.0);
app.undo_steps(1);
let main_redo = app.tabs[app.active_tab].history.redo_stack.len();
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.dispatch_command("BEDIT_DISCARD");
assert_eq!(members(&app).len(), 1);
assert_eq!(app.tabs[app.active_tab].history.redo_stack.len(), main_redo);
app.redo_steps(1);
assert_eq!(members(&app).len(), 1);
}
#[test]
fn dirty_close_cancel_discard_and_save_follow_the_checkpoint() {
let (mut app, reference) = fixture();
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.dispatch_command("BCLOSE");
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
let _ = app.update(Message::BlockEditChangesCancel);
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
assert_eq!(members(&app).len(), 2);
let _ = app.dispatch_command("BCLOSE");
let _ = app.update(Message::BlockEditChangesDiscard);
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
assert_eq!(members(&app).len(), 1);
assert_eq!(main_depth(&app), 0);
begin(&mut app, reference);
circle(&mut app, 2.0);
let _ = app.dispatch_command("BCLOSE");
let _ = app.update(Message::BlockEditChangesSave);
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
assert_eq!(members(&app).len(), 2);
assert_eq!(main_depth(&app), 1);
}
#[test]
fn clean_close_does_not_prompt_or_dirty_the_drawing() {
let (mut app, reference) = fixture();
begin(&mut app, reference);
assert!(!app.tabs[app.active_tab].dirty);
let _ = app.dispatch_command("BCLOSE");
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
assert!(app.active_modal.is_none());
assert!(!app.tabs[app.active_tab].dirty);
assert_eq!(main_depth(&app), 0);
}
#[test]
fn dirty_switch_can_cancel_and_never_nests_sessions() {
let (mut app, reference) = fixture();
let i = app.active_tab;
app.tabs[i]
.scene
.define_block_raw("OTHER", Vector3::ZERO, vec![]);
let other = app.tabs[i]
.scene
.document
.add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO)))
.unwrap();
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value()));
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
let _ = app.update(Message::BlockEditChangesCancel);
assert_eq!(
app.tabs[i].active_block_edit_session().unwrap().block_name,
"EDIT_ME"
);
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value()));
let _ = app.update(Message::BlockEditChangesSave);
assert_eq!(app.tabs[i].block_edits.len(), 1);
assert_eq!(
app.tabs[i].active_block_edit_session().unwrap().block_name,
"OTHER"
);
assert_eq!(main_depth(&app), 1);
}
#[test]
fn tab_close_prompts_for_block_edits_before_drawing_changes() {
let (mut app, reference) = fixture();
let id = app.tabs[app.active_tab].id;
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.update(Message::TabClose(id));
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
let _ = app.update(Message::BlockEditChangesCancel);
assert!(app.tabs.iter().any(|t| t.id == id));
let _ = app.update(Message::TabClose(id));
let _ = app.update(Message::BlockEditChangesDiscard);
assert!(!app.tabs.iter().any(|t| t.id == id));
}
#[test]
fn save_refuses_a_cycle_introduced_outside_the_insert_command() {
let (mut app, reference) = fixture();
let i = app.active_tab;
app.tabs[i].scene.define_block_raw(
"OTHER",
Vector3::ZERO,
vec![EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO))],
);
begin(&mut app, reference);
app.tabs[i]
.scene
.add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO)));
assert!(!app.save_block_edit(i));
assert_eq!(main_depth(&app), 0);
assert!(app.tabs[i].active_block_edit.is_some());
}
#[test]
fn drawing_history_retains_the_paper_layout_after_save_and_undo() {
let (mut app, reference) = fixture();
let i = app.active_tab;
app.tabs[i].scene.add_layout("Sheet").unwrap();
app.tabs[i].scene.set_current_layout("Sheet".into());
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.dispatch_command("BEDIT_SAVE");
assert_eq!(app.tabs[i].scene.current_layout, "Sheet");
assert!(!app.tabs[i].scene.document.header.show_model_space);
app.undo_steps(1);
assert_eq!(app.tabs[i].scene.current_layout, "Sheet");
assert!(!app.tabs[i].scene.document.header.show_model_space);
assert_eq!(
crate::io::saved_active_layout(&app.tabs[i].scene.document).as_deref(),
Some("Sheet")
);
assert_eq!(members(&app).len(), 1);
app.redo_steps(1);
assert!(!app.tabs[i].scene.document.header.show_model_space);
assert_eq!(members(&app).len(), 2);
}
#[test]
fn local_parameters_are_saved_and_discarded_with_the_definition() {
let (mut app, reference) = fixture();
begin(&mut app, reference);
let i = app.active_tab;
let scope = app.tabs[i].current_parametric_scope();
app.tabs[i]
.scene
.parametric_constraint_set_mut(scope)
.local_parameters
.set("Width", "7")
.unwrap();
assert!(app.save_block_edit(i));
app.tabs[i]
.scene
.parametric_constraint_set_mut(scope)
.local_parameters
.set("Width", "9")
.unwrap();
let _ = app.dispatch_command("BEDIT_DISCARD");
assert_eq!(
app.tabs[i]
.scene
.parametric_constraint_set(scope)
.unwrap()
.local_parameters
.resolve("Width")
.unwrap(),
7.0
);
app.undo_steps(1);
assert!(!app.tabs[i]
.scene
.parametric_constraint_set(scope)
.unwrap()
.local_parameters
.contains("Width"));
app.redo_steps(1);
assert_eq!(
app.tabs[i]
.scene
.parametric_constraint_set(scope)
.unwrap()
.local_parameters
.resolve("Width")
.unwrap(),
7.0
);
}
#[test]
fn quit_prompts_for_block_edits_before_unsaved_drawing_changes() {
let (mut app, reference) = fixture();
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.request_quit_with_block_edits();
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
let _ = app.update(Message::BlockEditChangesCancel);
assert!(app.pending_close.is_none());
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
let _ = app.request_quit_with_block_edits();
let _ = app.update(Message::BlockEditChangesSave);
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
assert_eq!(app.active_modal, Some(ModalKind::Unsaved));
assert!(matches!(app.pending_close, Some(super::PendingClose::Quit)));
}
#[test]
fn document_replacement_is_refused_until_the_editor_is_closed() {
let (mut app, reference) = fixture();
begin(&mut app, reference);
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], false);
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
}
#[test]
fn erasing_the_last_member_saves_an_empty_definition_and_undo_restores_it() {
let (mut app, reference) = fixture();
begin(&mut app, reference);
let i = app.active_tab;
let line = members(&app)[0];
app.push_undo_snapshot(i, "Erase block member");
app.tabs[i].scene.erase_entities(&[line]);
app.finish_pending_history(i);
assert!(members(&app).is_empty());
app.undo_steps(1);
assert_eq!(members(&app), vec![line]);
app.redo_steps(1);
assert!(members(&app).is_empty());
let _ = app.dispatch_command("BEDIT_SAVE");
assert!(members(&app).is_empty());
app.undo_steps(1);
assert_eq!(members(&app), vec![line]);
}
#[test]
fn close_all_resumes_after_discarding_a_dirty_block() {
let (mut app, reference) = fixture();
let first = app.tabs[app.active_tab].id;
app.tabs.push(super::document::DocumentTab::new_drawing(2));
let second = app.tabs.last().unwrap().id;
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.update(Message::DocTabCloseAll);
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
let _ = app.update(Message::BlockEditChangesDiscard);
assert!(!app
.tabs
.iter()
.any(|tab| tab.id == first || tab.id == second));
assert!(app.pending_tab_closes.is_empty());
}