diff --git a/crates/engine/src/tests/blendopts.rs b/crates/engine/src/tests/blendopts.rs index 7332e18ec..366a19241 100644 --- a/crates/engine/src/tests/blendopts.rs +++ b/crates/engine/src/tests/blendopts.rs @@ -234,31 +234,50 @@ fn escape_drops_the_blend_tools_first_pick() { } #[test] -fn an_edit_or_undo_made_outside_the_blend_tool_drops_its_first_pick() { - for undo in [false, true] { +fn an_edit_undo_or_redo_made_outside_the_blend_tool_drops_its_first_pick() { + for case in ["edit", "undo", "redo"] { + let undo = case == "undo"; let mut s = session(); let (a, b, c) = (rect(&mut s, 0.0, 0.0, 40.0), rect(&mut s, 200.0, 0.0, 40.0), rect(&mut s, 400.0, 0.0, 40.0)); + if case == "redo" { + s.execute("paint.setFill", &json!({"ids": [c.0], "color": "#ff0000"})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + } s.select_tool("blend", ViewInfo::default()).unwrap(); let click = |s: &mut Session, x: f64, y: f64| s.pointer(&PointerEvent::new(PointerKind::Down, x, y), ViewInfo::default()).unwrap(); - // Undo takes away c, the object picked first; an edit recolors c, which is not picked. + // Undo takes away c, the object picked first; an edit or a redo recolors c, which is not picked. click(&mut s, if undo { 420.0 } else { 20.0 }, 20.0); assert!(!s.overlays(ViewInfo::default()).is_empty()); - if undo { - s.execute("edit.undo", &json!({})).unwrap(); - } else { - s.execute("paint.setFill", &json!({"ids": [c.0], "color": "#ff0000"})).unwrap(); - } - assert!(s.overlays(ViewInfo::default()).is_empty(), "undo {undo}: the pick is not marked"); + match case { + "undo" => s.execute("edit.undo", &json!({})).unwrap(), + "redo" => s.execute("edit.redo", &json!({})).unwrap(), + _ => s.execute("paint.setFill", &json!({"ids": [c.0], "color": "#ff0000"})).unwrap(), + }; + assert!(s.overlays(ViewInfo::default()).is_empty(), "{case}: the pick is not marked"); let (layers, depth) = (s.doc().unwrap().doc.layers.clone(), s.doc().unwrap().history.undo.len()); click(&mut s, 220.0, 20.0); - assert_eq!(s.doc().unwrap().selection.objects, vec![b], "undo {undo}: b is picked first"); - assert_eq!((&s.doc().unwrap().doc.layers, s.doc().unwrap().history.undo.len()), (&layers, depth), "undo {undo}: the click changes no art"); + assert_eq!(s.doc().unwrap().selection.objects, vec![b], "{case}: b is picked first"); + assert_eq!((&s.doc().unwrap().doc.layers, s.doc().unwrap().history.undo.len()), (&layers, depth), "{case}: the click changes no art"); click(&mut s, if undo { 20.0 } else { 420.0 }, 20.0); let g = s.doc().unwrap().selection.objects[0]; - assert_eq!(spec(&s, g).0, if undo { vec![a, b] } else { vec![b, c] }, "undo {undo}"); + assert_eq!(spec(&s, g).0, if undo { vec![a, b] } else { vec![b, c] }, "{case}"); } } +#[test] +fn two_picks_with_no_edit_between_them_blend() { + let mut s = session(); + let (a, b) = (rect(&mut s, 0.0, 0.0, 40.0), rect(&mut s, 200.0, 0.0, 40.0)); + s.select_tool("blend", ViewInfo::default()).unwrap(); + let click = |s: &mut Session, x: f64, y: f64| s.pointer(&PointerEvent::new(PointerKind::Down, x, y), ViewInfo::default()).unwrap(); + click(&mut s, 20.0, 20.0); + // Drawing the overlays between the picks does not drop the first one. + assert!(!s.overlays(ViewInfo::default()).is_empty()); + click(&mut s, 220.0, 20.0); + let g = s.doc().unwrap().selection.objects[0]; + assert_eq!(spec(&s, g).0, vec![a, b]); +} + #[test] fn removing_the_blend_the_blend_tool_adds_to_ends_adding() { let mut s = session(); diff --git a/crates/engine/src/tests/history_mem.rs b/crates/engine/src/tests/history_mem.rs index af5d49552..23665d9ec 100644 --- a/crates/engine/src/tests/history_mem.rs +++ b/crates/engine/src/tests/history_mem.rs @@ -179,3 +179,40 @@ fn redo_during_an_interaction_keeps_the_history_and_the_document_in_step() { s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(first_x(&s), start); } + +/// `History::step` changes with every new step, Undo and Redo, also when an undo and a new edit +/// leave as many steps as before, and stays the same for an edit merged into an undo group and for +/// a selection change. +#[test] +fn the_history_step_changes_with_each_step_undo_and_redo() { + let mut s = session_with_rects(2); + let step = |s: &Session| s.doc().unwrap().history.step(); + let start = step(&s); + assert!(start.0 != 0 && start.1 == 0, "the Add step, and nothing to redo: {start:?}"); + s.execute("object.move", &json!({"dx": 1, "dy": 0})).unwrap(); + let moved = step(&s); + assert!(moved.0 != start.0 && moved.1 == 0, "{moved:?}"); + s.execute("edit.undo", &json!({})).unwrap(); + let undone = step(&s); + assert!(undone.0 == start.0 && undone.1 != 0, "{undone:?}"); + s.execute("edit.redo", &json!({})).unwrap(); + let redone = step(&s); + assert!(![start, moved, undone].contains(&redone) && redone.1 == 0, "{redone:?}"); + // An undo and a new edit: as many undo steps as after the move, at a different step. + s.execute("edit.undo", &json!({})).unwrap(); + let depth = s.doc().unwrap().history.undo.len(); + s.execute("object.move", &json!({"dx": 0, "dy": 1})).unwrap(); + let other = step(&s); + assert_eq!(s.doc().unwrap().history.undo.len(), depth + 1); + assert!(![start, moved, undone, redone].contains(&other), "{other:?}"); + s.execute("select.none", &json!({})).unwrap(); + assert_eq!(step(&s), other, "a selection change is no step"); + s.execute("select.all", &json!({})).unwrap(); + s.begin_undo_group(); + s.execute("object.move", &json!({"dx": 1, "dy": 0})).unwrap(); + let grouped = step(&s); + s.execute("object.move", &json!({"dx": 1, "dy": 0})).unwrap(); + assert_eq!(step(&s), grouped, "the second move is merged into the first"); + s.end_undo_group(false); + assert_ne!(grouped, other); +}