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"); +}