From 0b476e823d67d6bf8bf716031fdde35225d51fe3 Mon Sep 17 00:00:00 2001 From: Sazid Al Bayazid Date: Tue, 20 Oct 2026 00:55:43 +0600 Subject: [PATCH 1/2] Tests: the Options dialog's Cancel, Esc and the close button restore the settings (#878) The Options dialog applies every change as it is made, and closing it with Cancel, Esc or the window's close button puts the settings back the way they were. Nothing covered that, so a change to the dialog could have dropped it unnoticed. The kittest now covers all three ways out: a checkbox put back by Cancel, the appearance and the interface size put back by Esc, the same checkbox put back by the window's close button, and the dialog closing in each case. Red-proof: with the Cancel restore removed the checkbox test fails (the setting stays changed), with the Esc restore removed the appearance test fails (it stays dark), and with the Esc/close restore removed the close-button test fails (the setting stays changed). ROADMAP progress-log row added. --- ROADMAP.md | 1 + crates/ui-egui/src/dialogs.rs | 4 ++ crates/ui-egui/src/preferences_tests.rs | 95 +++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 crates/ui-egui/src/preferences_tests.rs diff --git a/ROADMAP.md b/ROADMAP.md index 93bc931e..542f7cd0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) | | 2026-10-17 | While a placeholder's text is being edited, a click on a shape in front of it selects that shape instead of moving the caret, as in PowerPoint (#931) | | 2026-10-15 | Native saves keep an imported chart's embedded workbook, drawings and relationships; headless PPTX → edit → native save/reopen → PPTX tests verify the payloads and stale-workbook rejection | +| 2026-10-20 | Tests: the Options dialog's Cancel, Esc and close button put the settings back the way they were (#878) | | 2026-10-15 | PPTX trigger sequences keep separate On Click steps, including paragraph builds; synthetic import/edit/save/reopen and headless show regressions | | 2026-10-16 | Export: PNG with a transparent background — the slide background is left out, only the slide's content is drawn (Export… dialog, File › Export, web download, `file.export`/`file.render` `transparentBackground`, `deckcraft-cli render --transparent`) (#784) | | 2026-10-16 | Arrange ▸ Align: Align to Slide and Align Selected Objects, as in PowerPoint. One object aligns to the slide and several to each other until the other choice is ticked; the choice holds for that selection and also drives Distribute | diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2393bd61..db345066 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3684,3 +3684,7 @@ mod spelling_tests; #[cfg(test)] #[path = "paragraph_dialog_tests.rs"] mod paragraph_dialog_tests; + +#[cfg(test)] +#[path = "preferences_tests.rs"] +mod preferences_tests; diff --git a/crates/ui-egui/src/preferences_tests.rs b/crates/ui-egui/src/preferences_tests.rs new file mode 100644 index 00000000..8740b6de --- /dev/null +++ b/crates/ui-egui/src/preferences_tests.rs @@ -0,0 +1,95 @@ +//! The Preferences (Options) dialog: controls apply as they change and Cancel, Esc and the +//! window's close button put the settings back the way they were (#752, reported as #878). + +use deckcraft_engine::Session; +use egui_kittest::kittest::{NodeT, Queryable}; +use egui_kittest::{Harness, Node}; +use serde_json::json; + +use crate::theme::Brightness; +use crate::{Services, SlideApp}; + +/// The slider itself, not the label drawn beside it. +fn interface_size<'a>(h: &'a Harness<'_, SlideApp>) -> Node<'a> { + h.get_all_by_label("Interface size").find(|n| n.accesskit_node().role() == egui::accesskit::Role::Slider).expect("the slider is there") +} + +fn harness() -> Harness<'static, SlideApp> { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + // The app keeps asking for frames, so step instead of run(). + h.run_steps(3); + h +} + +fn open_options(h: &mut Harness<'_, SlideApp>) { + h.state_mut().run("app.preferences", json!({})).expect("open Options"); + h.run_steps(3); +} + +fn dark(app: &SlideApp) -> bool { + app.ui.brightness == Brightness::Dark +} + +/// A checkbox applies as it is toggled and Cancel puts the old value back. +#[test] +fn options_cancel_undoes_the_changes() { + let mut h = harness(); + let was = h.state().session.prefs.smart_guides; + open_options(&mut h); + + h.query_by_label("Show smart guides").expect("the checkbox is there").click(); + h.run_steps(2); + assert_eq!(h.state().session.prefs.smart_guides, !was, "the change applies as it is made"); + + h.query_by_label("Cancel").expect("the Cancel button is there").click(); + h.run_steps(2); + assert_eq!(h.state().session.prefs.smart_guides, was, "Cancel restores the setting"); + assert!(h.state().dialog.is_none(), "and the dialog closes"); +} + +/// The appearance toggle and the interface size go back on Cancel, too; Esc acts as Cancel. +#[test] +fn options_cancel_puts_the_appearance_and_scale_back() { + let mut h = harness(); + let (dark_before, scale_before) = (dark(h.state()), h.state().ui.ui_scale); + open_options(&mut h); + + h.query_by_label("Dark appearance").expect("the checkbox is there").click(); + h.run_steps(2); + assert!(dark(h.state()), "the appearance applies as it is toggled"); + + // Clicking the slider's middle sets it near the middle of its range, away from the default. + interface_size(&h).click(); + h.run_steps(2); + let scale_in_dialog = h.state().ui.ui_scale; + assert!((scale_in_dialog - scale_before).abs() > 1e-3, "the slider applied a new size"); + + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert_eq!(dark(h.state()), dark_before, "Esc restores the appearance"); + assert!((h.state().ui.ui_scale - scale_before).abs() < 1e-3, "Esc restores the size"); + assert!(h.state().dialog.is_none(), "and the dialog closes"); +} + +/// The window's close button is the same as Cancel: the settings go back. +#[test] +fn options_close_button_restores_the_settings() { + let mut h = harness(); + let was = h.state().session.prefs.smart_guides; + open_options(&mut h); + + h.query_by_label("Show smart guides").expect("the checkbox is there").click(); + h.run_steps(2); + assert_eq!(h.state().session.prefs.smart_guides, !was, "the change applies as it is made"); + + h.get_all_by_label("Close window").next().expect("the window's close button is there").click(); + h.run_steps(2); + assert_eq!(h.state().session.prefs.smart_guides, was, "the close button restores the setting"); + assert!(h.state().dialog.is_none(), "and the dialog closes"); +} From dc69e57600c95d78330c83bb5fb7d2451b065fd4 Mon Sep 17 00:00:00 2001 From: Sazid Al Bayazid Date: Wed, 21 Oct 2026 00:35:43 +0600 Subject: [PATCH 2/2] Tests: Cancel restores every Options setting, not only the first (#878) A report on 0.7.0 said the Options dialog's Cancel did nothing, and a reproduction on 0.8.0 found it working for the appearance toggle and the name field, without trying the other controls. This covers the rest: the Editing checkboxes (snap to grid, smart quotes, autocorrect) and the name field go back with Cancel, like the first checkbox does. Verified red with the restore of the settings taken out: the name keeps what was typed. --- crates/ui-egui/src/preferences_tests.rs | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/ui-egui/src/preferences_tests.rs b/crates/ui-egui/src/preferences_tests.rs index 8740b6de..a0e4cba4 100644 --- a/crates/ui-egui/src/preferences_tests.rs +++ b/crates/ui-egui/src/preferences_tests.rs @@ -53,6 +53,42 @@ fn options_cancel_undoes_the_changes() { assert!(h.state().dialog.is_none(), "and the dialog closes"); } +/// Every Editing setting and the name field go back on Cancel, not only the first checkbox: a +/// reporter who changed a different control and saw Cancel do nothing has the same answer. +#[test] +fn options_cancel_undoes_every_editing_setting() { + let mut h = harness(); + let before = h.state().session.prefs.clone(); + open_options(&mut h); + + for label in ["Snap objects to grid", "Replace straight quotes with smart quotes", "AutoCorrect as you type"] { + h.query_by_label(label).unwrap_or_else(|| panic!("the {label} checkbox is there")).click(); + h.run_steps(1); + } + // The name field: it writes into the settings as it is typed. + // The name field is the last text input of the dialog (the label is a separate widget). + let name = h.get_all_by_role(egui::accesskit::Role::TextInput).last().expect("the name field is there"); + name.click(); + h.run_steps(1); + h.event(egui::Event::Text("Z".into())); + h.run_steps(1); + let changed = h.state().session.prefs.clone(); + assert_ne!( + (changed.snap_to_grid, changed.smart_quotes, changed.autocorrect), + (before.snap_to_grid, before.smart_quotes, before.autocorrect), + "the settings apply as they are changed" + ); + assert_ne!(changed.author, before.author, "the name applies as it is typed: {:?}", changed.author); + + h.query_by_label("Cancel").expect("the Cancel button is there").click(); + h.run_steps(2); + let after = h.state().session.prefs.clone(); + assert_eq!(after.snap_to_grid, before.snap_to_grid, "snap to grid is restored"); + assert_eq!(after.smart_quotes, before.smart_quotes, "smart quotes are restored"); + assert_eq!(after.autocorrect, before.autocorrect, "autocorrect is restored"); + assert_eq!(after.author, before.author, "the name is restored"); +} + /// The appearance toggle and the interface size go back on Cancel, too; Esc acts as Cancel. #[test] fn options_cancel_puts_the_appearance_and_scale_back() {