#1508: a refused backup from the menu says why

The menu bar turned a command's error into a toast only for exports, so
File ▸ Back Up Library… did nothing visible when the host refused (changes
still unsaved, or nothing stored in this browser); the refusal reached only
the control channel (found in a real-browser check of PR #1987). Backup and
restore refusals are toasted too. Menu items are registered for automation
as `menuitem:<command id>`, and a headless test clicks File ▸ Back Up
Library… against a refusing host and reads the toast.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
This commit is contained in:
Claude
2026-10-19 12:39:25 +00:00
parent abf29ea392
commit 4b8a5317fc
+28 -2
View File
@@ -962,9 +962,10 @@ pub fn show_in_window(app: &mut LightcraftApp, ui: &mut egui::Ui, max_width: f32
}
if let Some((id, params)) = clicked {
let r = run_item(app, &id, params);
// an export that can't start (e.g. no folder) says why instead of doing nothing
// an export that can't start (e.g. no folder), or a backup / restore the host refuses
// (changes still unsaved, nothing stored in this browser), says why instead of doing nothing
if let Err(e) = r
&& matches!(id.as_str(), "app.export" | "app.exportPrevious")
&& matches!(id.as_str(), "app.export" | "app.exportPrevious" | "file.backupLibrary" | "file.restoreLibrary")
{
app.toast(ui.ctx(), e);
}
@@ -1044,6 +1045,7 @@ fn nodes_ui(ui: &mut egui::Ui, nodes: &[MenuNode], mac: bool, clicked: &mut Opti
b = b.shortcut_text(shortcut_text(sc, mac));
}
let r = ui.add_enabled(*enabled, b);
crate::widgets::register(ui.ctx(), format!("menuitem:{id}"), r.rect);
if *checked == Some(true) {
let t = crate::theme::Tokens::get(ui.ctx());
let c = egui::Rect::from_center_size(egui::pos2(r.rect.left() + 11.0, r.rect.center().y), egui::vec2(13.0, 13.0));
@@ -1390,6 +1392,30 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// A backup the host refuses (changes still unsaved, issue #1508) says why when started from
/// the menu, as a refused export does; the refusal used to reach only the control channel.
#[test]
fn a_refused_backup_from_the_menu_shows_why() {
// the host refuses, as the web one does while changes are still unsaved (issue #1508)
let refusal = "Backup not started: 1 change could not be saved to browser storage yet";
let services = crate::Services {
backup_library: Some(Box::new(move |_: &mut lightcraft_engine::Session| Err(refusal.to_string()))),
..Default::default()
};
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
let mut h = crate::headless::Headless::new(app, [1400.0, 900.0], 1.0);
let t = std::time::Duration::from_secs(30);
h.settle(t);
for id in ["menu:File", "menuitem:file.backupLibrary"] {
let r = h.request("ui.clickWidget", json!({"id": id}), t);
assert_eq!(r["ok"], true, "{id}: {r}");
h.step();
}
// (read at once: a toast keeps the UI repainting until it expires, so settling would outlive it)
let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();
assert!(toast.contains(refusal), "the refusal is shown: {toast:?}");
}
/// #1528: `file.findMissing` with `wait: true` must propagate folder validation and search
/// failures as errors instead of returning `Ok(null)`.
#[test]