Files
soundcraft/crates/ui-egui/tests/save_changes.rs
T
grovebit 18a2558104 Ask to save changes before closing a modified session
Close Session, New, Open, Revert, Quit, the window's close button,
dropping a session file on the window and opening one from the Workspace
window all replaced or closed the session without asking, so unsaved
edits were lost (#136). They now ask first: Save, Don't Save or Cancel.
Save on a session that has never been saved goes through Save As, then
carries on. Revert asks for confirmation instead, and Revert to Saved is
unavailable until the session has a file. A take being recorded is
stopped first, so it counts as a change. The web build has nowhere to
save, so it offers only Don't Save and Cancel. Calls from the control
channel, MCP and the CLI never ask.

Selecting, locating, zooming and scrolling no longer mark the session
modified, so the question only comes after real edits. Undoing back to
the saved state makes the session clean again. The engine compares the
document with the one last opened or saved, ignoring the selection, the
playhead and the zoom, once per revision.

Fixes #136
2026-10-11 20:13:48 +01:00

98 lines
3.3 KiB
Rust

use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
use soundcraft_ui_egui::dialogs::Dialog;
use soundcraft_ui_egui::{Services, SoundApp, menus};
fn harness() -> Harness<'static, SoundApp> {
let app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_ui_state(
|ui, app: &mut SoundApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
app,
)
}
fn steps(h: &mut Harness<'_, SoundApp>) {
for _ in 0..4 {
h.step();
}
}
fn click(h: &mut Harness<'_, SoundApp>, label: &str) {
h.get_by_label(label).click();
steps(h);
}
fn close_session(h: &mut Harness<'_, SoundApp>) {
menus::invoke_menu(h.state_mut(), "session.close", "File > Close Session");
steps(h);
}
fn mute_kick(h: &mut Harness<'_, SoundApp>) {
h.state_mut().run("mix.mute", json!({"track": "Kick"})).unwrap();
}
#[test]
fn closing_a_modified_session_asks_first() {
let mut h = harness();
let tracks = h.state().engine.session().tracks.len();
h.state_mut().run("edit.select", json!({"tracks": ["Kick"], "start": 0, "end": 48_000})).unwrap();
close_session(&mut h);
assert!(h.state().dialogs.open.is_none(), "a selection alone is not a change");
assert!(h.state().engine.session().tracks.is_empty());
let mut h = harness();
mute_kick(&mut h);
close_session(&mut h);
assert_eq!(h.state().dialogs.open_name(), Some("save_changes"));
click(&mut h, "Cancel");
assert!(h.state().dialogs.open.is_none());
assert_eq!(h.state().engine.session().tracks.len(), tracks);
close_session(&mut h);
click(&mut h, "Don't Save");
assert!(h.state().engine.session().tracks.is_empty());
let mut h = harness();
mute_kick(&mut h);
menus::invoke_menu(h.state_mut(), "app.quit", "");
steps(&mut h);
assert!(!h.state().quit_requested);
click(&mut h, "Don't Save");
assert!(h.state().quit_requested);
}
#[test]
fn save_in_the_question_saves_before_closing() {
let dir = std::env::temp_dir().join(format!("soundcraft-save-changes-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("Song.scraft").to_string_lossy().into_owned();
let mut h = harness();
mute_kick(&mut h);
close_session(&mut h);
click(&mut h, "Save");
match &mut h.state_mut().dialogs.open {
Some(Dialog::PathPrompt { cmd, path: p, .. }) if cmd == "session.save_as" => p.clone_from(&path),
other => panic!("expected Save As, got {other:?}"),
}
steps(&mut h);
click(&mut h, "OK");
assert!(h.state().engine.session().tracks.is_empty());
h.state_mut().run("session.open", json!({"path": path})).unwrap();
assert!(h.state().engine.session().track_by_name("Kick").unwrap().mixer.mute);
h.state_mut().run("mix.mute", json!({"track": "Kick"})).unwrap();
close_session(&mut h);
click(&mut h, "Save");
assert!(h.state().dialogs.open.is_none());
assert!(h.state().engine.session().tracks.is_empty());
h.state_mut().run("session.open", json!({"path": path})).unwrap();
assert!(!h.state().engine.session().track_by_name("Kick").unwrap().mixer.mute);
let _ = std::fs::remove_dir_all(&dir);
}