diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index 384a8dd..ee27773 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -63,7 +63,6 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HBW", Entry::Finish(Action::Command("home.drawBorder"))), // Excel: Draw Border ("HBY", Entry::Finish(Action::Command("home.borders"))), // Excel: Line Style; Line Style (submenu TBD) ("HC", Entry::Finish(Action::Command("edit.copy"))), // Excel: Copy; fires immediately in Excel; deeper aliases dropped as unreachable - ("HCP", Entry::Finish(Action::Pending("home.copyAsPicture"))), // pending; Excel: Copy as Picture; reserved until the command lands ("HDC", Entry::Finish(Action::Command("home.deleteColumns"))), // Excel: Delete Columns; Delete Columns ("HDD", Entry::Finish(Action::Command("home.deleteCells"))), // Excel: Delete Cells ("HDR", Entry::Finish(Action::Command("home.deleteRows"))), // Excel: Delete Rows; Delete Rows @@ -172,7 +171,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HVV", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values; paste variant, params TBD: what=values ("HVW", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Keep Source Column Widths; paste variant, params TBD: what=TBD(col-widths) ("HW", Entry::Continue), // Excel: Wrap Text Menu; group level: containment(Wrap Text) - ("HWS", Entry::Finish(Action::Pending("home.shrinkToFit"))), // pending; Excel: Shrink to Fit; Shrink to Fit; reserved until the command lands + ("HWS", Entry::Finish(Action::Command("home.shrinkToFit"))), // Excel: Shrink to Fit; landed in the catalog; live since T7 probe ("HWW", Entry::Finish(Action::Command("home.wrapText"))), // Excel: Wrap Text ("HX", Entry::Finish(Action::Command("edit.cut"))), // Excel: Cut ("NC1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Column Chart; Column chart (kind TBD) @@ -245,7 +244,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("PVG", Entry::Finish(Action::Command("view.gridlines"))), // Excel: View Gridlines ("PVH", Entry::Finish(Action::Command("view.headings"))), // Excel: View Headings ("MA", Entry::Continue), // Excel: Remove Arrows Menu; group level: containment(Remove Arrows) - ("MAA", Entry::Finish(Action::Pending("formulas.removeArrows"))), // pending; Excel: Remove All Arrows; Remove All Arrows; reserved until the command lands + ("MAA", Entry::Finish(Action::Command("formulas.removeArrows"))), // Excel: Remove All Arrows; landed in the catalog; live since T7 probe ("MAP", Entry::Finish(Action::Pending("formulas.removePrecedentArrows"))), // pending; Excel: Remove Precedent Arrows; Remove Precedent Arrows; reserved until the command lands ("MB", Entry::Finish(Action::Command("formulas.calculateNow"))), // Excel: Calculate Now ("MC", Entry::Finish(Action::Command("formulas.createFromSelection"))), // Excel: Create From Selection diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index 5dd9bcc..555377b 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -380,14 +380,6 @@ "note": "fires immediately in Excel; deeper aliases dropped as unreachable", "target": "edit.copy" }, - { - "seq": "HCP", - "kind": "command", - "target": "home.copyAsPicture", - "pending": true, - "excel": "Copy as Picture", - "note": "reserved until the command lands" - }, { "seq": "HDC", "kind": "command", @@ -1141,9 +1133,8 @@ "seq": "HWS", "kind": "command", "target": "home.shrinkToFit", - "pending": true, "excel": "Shrink to Fit", - "note": "Shrink to Fit; reserved until the command lands" + "note": "landed in the catalog; live since T7 probe" }, { "seq": "HWW", @@ -1648,9 +1639,8 @@ "seq": "MAA", "kind": "command", "target": "formulas.removeArrows", - "pending": true, "excel": "Remove All Arrows", - "note": "Remove All Arrows; reserved until the command lands" + "note": "landed in the catalog; live since T7 probe" }, { "seq": "MAP", diff --git a/crates/ui-egui/tests/keytips_generated.rs b/crates/ui-egui/tests/keytips_generated.rs new file mode 100644 index 0000000..bcc3bdf --- /dev/null +++ b/crates/ui-egui/tests/keytips_generated.rs @@ -0,0 +1,3165 @@ +//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file. +//! +//! One end-to-end test per keytip sequence, through the real egui input path. +//! Assertions are generated from a probe of the live engine catalog (blank +//! workbook, empty params), so each test proves exactly what its command does +//! today: journaled, ran clean, surfaced a missing-params error, opened a +//! dialog, switched tabs, waited for more input, or stayed silent (pending). + +use egui::os::OperatingSystem; +use egui::{Event, Key, Modifiers}; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness(session: Session) -> Harness { + let mut h = egui_kittest::Harness::builder().with_os(OperatingSystem::Nix).with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +fn blank() -> Session { + let mut s = Session::new(); + s.new_workbook(); + s +} + +fn key_event(key: Key, pressed: bool, modifiers: Modifiers) -> Event { + Event::Key { key, physical_key: None, pressed, repeat: false, modifiers } +} + +fn events(h: &mut Harness, events: impl IntoIterator) { + h.input_mut().events.extend(events); + h.run_steps(2); +} + +fn key(h: &mut Harness, key: Key) { + events(h, [key_event(key, true, Modifiers::NONE), key_event(key, false, Modifiers::NONE)]); +} + +fn letter(h: &mut Harness, key: Key, text_first: bool) { + let text = Event::Text(key.name().to_ascii_lowercase()); + let pressed = key_event(key, true, Modifiers::NONE); + let pair = if text_first { [text, pressed] } else { [pressed, text] }; + events(h, pair.into_iter().chain([key_event(key, false, Modifiers::NONE)])); +} + +fn sequence(h: &mut Harness, seq: &str, text_first: bool) { + key(h, Key::F10); + for ch in seq.chars() { + let k = Key::from_name(&ch.to_string()).expect("sequence letters map to keys"); + letter(h, k, text_first); + } +} + +#[test] +fn keytip_h_() { + let mut h = harness(blank()); + sequence(&mut h, "H", false); + assert_eq!(h.state().ui.ribbon_tab, "Home", "H must switch to the Home tab"); +} + +#[test] +fn keytip_n_() { + let mut h = harness(blank()); + sequence(&mut h, "N", false); + assert_eq!(h.state().ui.ribbon_tab, "Insert", "N must switch to the Insert tab"); +} + +#[test] +fn keytip_ji_() { + let mut h = harness(blank()); + sequence(&mut h, "JI", false); + assert_eq!(h.state().ui.ribbon_tab, "Draw", "JI must switch to the Draw tab"); +} + +#[test] +fn keytip_p_() { + let mut h = harness(blank()); + sequence(&mut h, "P", false); + assert_eq!(h.state().ui.ribbon_tab, "Page Layout", "P must switch to the Page Layout tab"); +} + +#[test] +fn keytip_m_() { + let mut h = harness(blank()); + sequence(&mut h, "M", false); + assert_eq!(h.state().ui.ribbon_tab, "Formulas", "M must switch to the Formulas tab"); +} + +#[test] +fn keytip_a_() { + let mut h = harness(blank()); + sequence(&mut h, "A", false); + assert_eq!(h.state().ui.ribbon_tab, "Data", "A must switch to the Data tab"); +} + +#[test] +fn keytip_r_() { + let mut h = harness(blank()); + sequence(&mut h, "R", false); + assert_eq!(h.state().ui.ribbon_tab, "Review", "R must switch to the Review tab"); +} + +#[test] +fn keytip_w_() { + let mut h = harness(blank()); + sequence(&mut h, "W", false); + assert_eq!(h.state().ui.ribbon_tab, "View", "W must switch to the View tab"); +} + +#[test] +fn keytip_e_() { + let mut h = harness(blank()); + sequence(&mut h, "E", false); + assert_eq!(h.state().ui.ribbon_tab, "Home", "E must switch to the Home tab"); +} + +#[test] +fn keytip_o_() { + let mut h = harness(blank()); + sequence(&mut h, "O", false); + assert_eq!(h.state().ui.ribbon_tab, "Home", "O must switch to the Home tab"); +} + +#[test] +fn keytip_har_() { + let mut h = harness(blank()); + sequence(&mut h, "HAR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignRight"), "HAR must journal home.alignRight"); +} + +#[test] +fn keytip_hat_() { + let mut h = harness(blank()); + sequence(&mut h, "HAT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignTop"), "HAT must journal home.alignTop"); +} + +#[test] +fn keytip_ham_() { + let mut h = harness(blank()); + sequence(&mut h, "HAM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignMiddle"), "HAM must journal home.alignMiddle"); +} + +#[test] +fn keytip_hab_() { + let mut h = harness(blank()); + sequence(&mut h, "HAB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignBottom"), "HAB must journal home.alignBottom"); +} + +#[test] +fn keytip_hal_() { + let mut h = harness(blank()); + sequence(&mut h, "HAL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignLeft"), "HAL must journal home.alignLeft"); +} + +#[test] +fn keytip_hac_() { + let mut h = harness(blank()); + sequence(&mut h, "HAC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignCenter"), "HAC must journal home.alignCenter"); +} + +#[test] +fn keytip_hvs_() { + let mut h = harness(blank()); + sequence(&mut h, "HVS", false); + assert!(h.state().dialog.is_some(), "HVS must open the pasteSpecial dialog"); +} + +#[test] +fn keytip_es_() { + let mut h = harness(blank()); + sequence(&mut h, "ES", false); + assert!(h.state().dialog.is_some(), "ES must open the pasteSpecial dialog"); +} + +#[test] +fn keytip_hoi_() { + let mut h = harness(blank()); + sequence(&mut h, "HOI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.autofitColumnWidth"), "HOI must journal home.autofitColumnWidth"); +} + +#[test] +fn keytip_oca_() { + let mut h = harness(blank()); + sequence(&mut h, "OCA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.autofitColumnWidth"), "OCA must journal home.autofitColumnWidth"); +} + +#[test] +fn keytip_wvg_() { + let mut h = harness(blank()); + sequence(&mut h, "WVG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines"), "WVG must journal view.gridlines"); +} + +#[test] +fn keytip_0_() { + let mut h = harness(blank()); + sequence(&mut h, "0", false); + assert!(h.state().dialog.is_none(), "0 must not open a dialog"); + assert!(h.state().message.is_none(), "0 must not open a message box"); + assert!(h.state().toast.is_none(), "0 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "0 must not journal"); +} + +#[test] +fn keytip_1_() { + let mut h = harness(blank()); + sequence(&mut h, "1", false); + assert!(h.state().dialog.is_none(), "1 must not open a dialog"); + assert!(h.state().message.is_none(), "1 must not open a message box"); + assert!(h.state().toast.is_none(), "1 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "1 must not journal"); +} + +#[test] +fn keytip_2_() { + let mut h = harness(blank()); + sequence(&mut h, "2", false); + assert!(h.state().dialog.is_none(), "2 must not open a dialog"); + assert!(h.state().message.is_none(), "2 must not open a message box"); + assert!(h.state().toast.is_none(), "2 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "2 must not journal"); +} + +#[test] +fn keytip_3_() { + let mut h = harness(blank()); + sequence(&mut h, "3", false); + assert!(h.state().dialog.is_none(), "3 must not open a dialog"); + assert!(h.state().message.is_none(), "3 must not open a message box"); + assert!(h.state().toast.is_none(), "3 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "3 must not journal"); +} + +#[test] +fn keytip_4_() { + let mut h = harness(blank()); + sequence(&mut h, "4", false); + assert!(h.state().dialog.is_none(), "4 must not open a dialog"); + assert!(h.state().message.is_none(), "4 must not open a message box"); + assert!(h.state().toast.is_none(), "4 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "4 must not journal"); +} + +#[test] +fn keytip_5_() { + let mut h = harness(blank()); + sequence(&mut h, "5", false); + assert!(h.state().dialog.is_none(), "5 must not open a dialog"); + assert!(h.state().message.is_none(), "5 must not open a message box"); + assert!(h.state().toast.is_none(), "5 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "5 must not journal"); +} + +#[test] +fn keytip_h0_() { + let mut h = harness(blank()); + sequence(&mut h, "H0", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.increaseDecimal"), "H0 must journal home.increaseDecimal"); +} + +#[test] +fn keytip_h1_() { + let mut h = harness(blank()); + sequence(&mut h, "H1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.bold"), "H1 must journal home.bold"); +} + +#[test] +fn keytip_h2_() { + let mut h = harness(blank()); + sequence(&mut h, "H2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.italic"), "H2 must journal home.italic"); +} + +#[test] +fn keytip_h3_() { + let mut h = harness(blank()); + sequence(&mut h, "H3", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.underline"), "H3 must journal home.underline"); +} + +#[test] +fn keytip_h5_() { + let mut h = harness(blank()); + sequence(&mut h, "H5", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.decreaseIndent"), "H5 must journal home.decreaseIndent"); +} + +#[test] +fn keytip_h6_() { + let mut h = harness(blank()); + sequence(&mut h, "H6", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.increaseIndent"), "H6 must journal home.increaseIndent"); +} + +#[test] +fn keytip_h9_() { + let mut h = harness(blank()); + sequence(&mut h, "H9", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.decreaseDecimal"), "H9 must journal home.decreaseDecimal"); +} + +#[test] +fn keytip_han_() { + let mut h = harness(blank()); + sequence(&mut h, "HAN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.accounting"), "HAN must journal home.accounting"); +} + +#[test] +fn keytip_has_() { + let mut h = harness(blank()); + sequence(&mut h, "HAS", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HAS must run home.addIns cleanly"); +} + +#[test] +fn keytip_hb_() { + let mut h = harness(blank()); + sequence(&mut h, "HB", false); + assert_eq!(h.state().keytips.prefix(), Some("HB"), "HB must stay armed waiting for more input"); +} + +#[test] +fn keytip_hba_() { + let mut h = harness(blank()); + sequence(&mut h, "HBA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBA must journal home.borders"); +} + +#[test] +fn keytip_hbb_() { + let mut h = harness(blank()); + sequence(&mut h, "HBB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBB must journal home.borders"); +} + +#[test] +fn keytip_hbc_() { + let mut h = harness(blank()); + sequence(&mut h, "HBC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBC must journal home.borders"); +} + +#[test] +fn keytip_hbd_() { + let mut h = harness(blank()); + sequence(&mut h, "HBD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBD must journal home.borders"); +} + +#[test] +fn keytip_hbe_() { + let mut h = harness(blank()); + sequence(&mut h, "HBE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBE must journal home.borders"); +} + +#[test] +fn keytip_hbg_() { + let mut h = harness(blank()); + sequence(&mut h, "HBG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.drawBorder"), "HBG must journal home.drawBorder"); +} + +#[test] +fn keytip_hbh_() { + let mut h = harness(blank()); + sequence(&mut h, "HBH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBH must journal home.borders"); +} + +#[test] +fn keytip_hbi_() { + let mut h = harness(blank()); + sequence(&mut h, "HBI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBI must journal home.borders"); +} + +#[test] +fn keytip_hbl_() { + let mut h = harness(blank()); + sequence(&mut h, "HBL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBL must journal home.borders"); +} + +#[test] +fn keytip_hbm_() { + let mut h = harness(blank()); + sequence(&mut h, "HBM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBM must journal home.borders"); +} + +#[test] +fn keytip_hbn_() { + let mut h = harness(blank()); + sequence(&mut h, "HBN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBN must journal home.borders"); +} + +#[test] +fn keytip_hbo_() { + let mut h = harness(blank()); + sequence(&mut h, "HBO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBO must journal home.borders"); +} + +#[test] +fn keytip_hbp_() { + let mut h = harness(blank()); + sequence(&mut h, "HBP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBP must journal home.borders"); +} + +#[test] +fn keytip_hbr_() { + let mut h = harness(blank()); + sequence(&mut h, "HBR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBR must journal home.borders"); +} + +#[test] +fn keytip_hbs_() { + let mut h = harness(blank()); + sequence(&mut h, "HBS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBS must journal home.borders"); +} + +#[test] +fn keytip_hbt_() { + let mut h = harness(blank()); + sequence(&mut h, "HBT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBT must journal home.borders"); +} + +#[test] +fn keytip_hbu_() { + let mut h = harness(blank()); + sequence(&mut h, "HBU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBU must journal home.borders"); +} + +#[test] +fn keytip_hbw_() { + let mut h = harness(blank()); + sequence(&mut h, "HBW", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.drawBorder"), "HBW must journal home.drawBorder"); +} + +#[test] +fn keytip_hby_() { + let mut h = harness(blank()); + sequence(&mut h, "HBY", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBY must journal home.borders"); +} + +#[test] +fn keytip_hc_() { + let mut h = harness(blank()); + sequence(&mut h, "HC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.copy"), "HC must journal edit.copy"); +} + +#[test] +fn keytip_hdc_() { + let mut h = harness(blank()); + sequence(&mut h, "HDC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.deleteColumns"), "HDC must journal home.deleteColumns"); +} + +#[test] +fn keytip_hdd_() { + let mut h = harness(blank()); + sequence(&mut h, "HDD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.deleteCells"), "HDD must journal home.deleteCells"); +} + +#[test] +fn keytip_hdr_() { + let mut h = harness(blank()); + sequence(&mut h, "HDR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.deleteRows"), "HDR must journal home.deleteRows"); +} + +#[test] +fn keytip_hds_() { + let mut h = harness(blank()); + sequence(&mut h, "HDS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HDS must invoke home.deleteSheet and surface its missing-params error"); +} + +#[test] +fn keytip_hea_() { + let mut h = harness(blank()); + sequence(&mut h, "HEA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearAll"), "HEA must journal edit.clearAll"); +} + +#[test] +fn keytip_hec_() { + let mut h = harness(blank()); + sequence(&mut h, "HEC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearContents"), "HEC must journal edit.clearContents"); +} + +#[test] +fn keytip_hef_() { + let mut h = harness(blank()); + sequence(&mut h, "HEF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearFormats"), "HEF must journal edit.clearFormats"); +} + +#[test] +fn keytip_hel_() { + let mut h = harness(blank()); + sequence(&mut h, "HEL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearHyperlinks"), "HEL must journal edit.clearHyperlinks"); +} + +#[test] +fn keytip_hem_() { + let mut h = harness(blank()); + sequence(&mut h, "HEM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearComments"), "HEM must journal edit.clearComments"); +} + +#[test] +fn keytip_hfc_() { + let mut h = harness(blank()); + sequence(&mut h, "HFC", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFC must invoke home.fontColor and surface its missing-params error"); +} + +#[test] +fn keytip_hfdc_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HFDC must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hfdf_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDF must invoke edit.find and surface its missing-params error"); +} + +#[test] +fn keytip_hfdg_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDG", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDG must invoke edit.goTo and surface its missing-params error"); +} + +#[test] +fn keytip_hfdm_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDM must invoke edit.find and surface its missing-params error"); +} + +#[test] +fn keytip_hfdo_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDO", false); + assert!(h.state().dialog.is_none(), "HFDO must not open a dialog"); + assert!(h.state().message.is_none(), "HFDO must not open a message box"); + assert!(h.state().toast.is_none(), "HFDO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFDO must not journal"); +} + +#[test] +fn keytip_hfdp_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDP", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HFDP must run arrange.selectionPane cleanly"); +} + +#[test] +fn keytip_hfdr_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDR must invoke edit.replace and surface its missing-params error"); +} + +#[test] +fn keytip_hfds_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.goToSpecial"), "HFDS must journal edit.goToSpecial"); +} + +#[test] +fn keytip_hff_() { + let mut h = harness(blank()); + sequence(&mut h, "HFF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFF must invoke home.fontName and surface its missing-params error"); +} + +#[test] +fn keytip_hfg_() { + let mut h = harness(blank()); + sequence(&mut h, "HFG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.increaseFontSize"), "HFG must journal home.increaseFontSize"); +} + +#[test] +fn keytip_hfi_() { + let mut h = harness(blank()); + sequence(&mut h, "HFI", false); + assert_eq!(h.state().keytips.prefix(), Some("HFI"), "HFI must stay armed waiting for more input"); +} + +#[test] +fn keytip_hfia_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIA", false); + assert!(h.state().dialog.is_none(), "HFIA must not open a dialog"); + assert!(h.state().message.is_none(), "HFIA must not open a message box"); + assert!(h.state().toast.is_none(), "HFIA must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFIA must not journal"); +} + +#[test] +fn keytip_hfid_() { + let mut h = harness(blank()); + sequence(&mut h, "HFID", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillDown"), "HFID must journal edit.fillDown"); +} + +#[test] +fn keytip_hfif_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFIF must invoke edit.flashFill and surface its missing-params error"); +} + +#[test] +fn keytip_hfij_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIJ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.justify"), "HFIJ must journal home.justify"); +} + +#[test] +fn keytip_hfil_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillLeft"), "HFIL must journal edit.fillLeft"); +} + +#[test] +fn keytip_hfir_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillRight"), "HFIR must journal edit.fillRight"); +} + +#[test] +fn keytip_hfis_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillSeries"), "HFIS must journal edit.fillSeries"); +} + +#[test] +fn keytip_hfiu_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillUp"), "HFIU must journal edit.fillUp"); +} + +#[test] +fn keytip_hfk_() { + let mut h = harness(blank()); + sequence(&mut h, "HFK", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.decreaseFontSize"), "HFK must journal home.decreaseFontSize"); +} + +#[test] +fn keytip_hfm_() { + let mut h = harness(blank()); + sequence(&mut h, "HFM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFM must invoke home.numberFormat and surface its missing-params error"); +} + +#[test] +fn keytip_hfp_() { + let mut h = harness(blank()); + sequence(&mut h, "HFP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.formatPainter"), "HFP must journal edit.formatPainter"); +} + +#[test] +fn keytip_hfq_() { + let mut h = harness(blank()); + sequence(&mut h, "HFQ", false); + assert_eq!(h.state().keytips.prefix(), Some("HFQ"), "HFQ must stay armed waiting for more input"); +} + +#[test] +fn keytip_hfql_() { + let mut h = harness(blank()); + sequence(&mut h, "HFQL", false); + assert!(h.state().dialog.is_none(), "HFQL must not open a dialog"); + assert!(h.state().message.is_none(), "HFQL must not open a message box"); + assert!(h.state().toast.is_none(), "HFQL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFQL must not journal"); +} + +#[test] +fn keytip_hfqo_() { + let mut h = harness(blank()); + sequence(&mut h, "HFQO", false); + assert!(h.state().dialog.is_none(), "HFQO must not open a dialog"); + assert!(h.state().message.is_none(), "HFQO must not open a message box"); + assert!(h.state().toast.is_none(), "HFQO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFQO must not journal"); +} + +#[test] +fn keytip_hfs_() { + let mut h = harness(blank()); + sequence(&mut h, "HFS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFS must invoke home.fontSize and surface its missing-params error"); +} + +#[test] +fn keytip_hh_() { + let mut h = harness(blank()); + sequence(&mut h, "HH", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HH must invoke home.fillColor and surface its missing-params error"); +} + +#[test] +fn keytip_hic_() { + let mut h = harness(blank()); + sequence(&mut h, "HIC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertColumns"), "HIC must journal home.insertColumns"); +} + +#[test] +fn keytip_hie_() { + let mut h = harness(blank()); + sequence(&mut h, "HIE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertCells"), "HIE must journal home.insertCells"); +} + +#[test] +fn keytip_hii_() { + let mut h = harness(blank()); + sequence(&mut h, "HII", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertCells"), "HII must journal home.insertCells"); +} + +#[test] +fn keytip_hir_() { + let mut h = harness(blank()); + sequence(&mut h, "HIR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertRows"), "HIR must journal home.insertRows"); +} + +#[test] +fn keytip_his_() { + let mut h = harness(blank()); + sequence(&mut h, "HIS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertSheet"), "HIS must journal home.insertSheet"); +} + +#[test] +fn keytip_hj_() { + let mut h = harness(blank()); + sequence(&mut h, "HJ", false); + assert_eq!(h.state().keytips.prefix(), Some("HJ"), "HJ must stay armed waiting for more input"); +} + +#[test] +fn keytip_hjn_() { + let mut h = harness(blank()); + sequence(&mut h, "HJN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HJN must invoke home.newCellStyle and surface its missing-params error"); +} + +#[test] +fn keytip_hk_() { + let mut h = harness(blank()); + sequence(&mut h, "HK", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.comma"), "HK must journal home.comma"); +} + +#[test] +fn keytip_hl_() { + let mut h = harness(blank()); + sequence(&mut h, "HL", false); + assert_eq!(h.state().keytips.prefix(), Some("HL"), "HL must stay armed waiting for more input"); +} + +#[test] +fn keytip_hlc_() { + let mut h = harness(blank()); + sequence(&mut h, "HLC", false); + assert_eq!(h.state().keytips.prefix(), Some("HLC"), "HLC must stay armed waiting for more input"); +} + +#[test] +fn keytip_hlce_() { + let mut h = harness(blank()); + sequence(&mut h, "HLCE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.clearRules"), "HLCE must journal home.clearRules"); +} + +#[test] +fn keytip_hlcs_() { + let mut h = harness(blank()); + sequence(&mut h, "HLCS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.clearRules"), "HLCS must journal home.clearRules"); +} + +#[test] +fn keytip_hlct_() { + let mut h = harness(blank()); + sequence(&mut h, "HLCT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.clearRules"), "HLCT must journal home.clearRules"); +} + +#[test] +fn keytip_hld_() { + let mut h = harness(blank()); + sequence(&mut h, "HLD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLD must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hlh_() { + let mut h = harness(blank()); + sequence(&mut h, "HLH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLH must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hln_() { + let mut h = harness(blank()); + sequence(&mut h, "HLN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLN must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hlr_() { + let mut h = harness(blank()); + sequence(&mut h, "HLR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.manageRules"), "HLR must journal home.manageRules"); +} + +#[test] +fn keytip_hlt_() { + let mut h = harness(blank()); + sequence(&mut h, "HLT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLT must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hm_() { + let mut h = harness(blank()); + sequence(&mut h, "HM", false); + assert_eq!(h.state().keytips.prefix(), Some("HM"), "HM must stay armed waiting for more input"); +} + +#[test] +fn keytip_hma_() { + let mut h = harness(blank()); + sequence(&mut h, "HMA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.mergeAcross"), "HMA must journal home.mergeAcross"); +} + +#[test] +fn keytip_hmc_() { + let mut h = harness(blank()); + sequence(&mut h, "HMC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.mergeCenter"), "HMC must journal home.mergeCenter"); +} + +#[test] +fn keytip_hmm_() { + let mut h = harness(blank()); + sequence(&mut h, "HMM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.mergeCells"), "HMM must journal home.mergeCells"); +} + +#[test] +fn keytip_hmu_() { + let mut h = harness(blank()); + sequence(&mut h, "HMU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.unmergeCells"), "HMU must journal home.unmergeCells"); +} + +#[test] +fn keytip_hn_() { + let mut h = harness(blank()); + sequence(&mut h, "HN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HN must invoke home.numberFormat and surface its missing-params error"); +} + +#[test] +fn keytip_hoa_() { + let mut h = harness(blank()); + sequence(&mut h, "HOA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.autofitRowHeight"), "HOA must journal home.autofitRowHeight"); +} + +#[test] +fn keytip_hod_() { + let mut h = harness(blank()); + sequence(&mut h, "HOD", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOD must invoke home.columnWidth and surface its missing-params error"); +} + +#[test] +fn keytip_hoe_() { + let mut h = harness(blank()); + sequence(&mut h, "HOE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.formatCells"), "HOE must journal home.formatCells"); +} + +#[test] +fn keytip_hoh_() { + let mut h = harness(blank()); + sequence(&mut h, "HOH", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOH must invoke home.rowHeight and surface its missing-params error"); +} + +#[test] +fn keytip_hol_() { + let mut h = harness(blank()); + sequence(&mut h, "HOL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.lockCell"), "HOL must journal home.lockCell"); +} + +#[test] +fn keytip_hom_() { + let mut h = harness(blank()); + sequence(&mut h, "HOM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOM must invoke sheet.move and surface its missing-params error"); +} + +#[test] +fn keytip_hop_() { + let mut h = harness(blank()); + sequence(&mut h, "HOP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectSheet"), "HOP must journal review.protectSheet"); +} + +#[test] +fn keytip_hor_() { + let mut h = harness(blank()); + sequence(&mut h, "HOR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOR must invoke sheet.rename and surface its missing-params error"); +} + +#[test] +fn keytip_hot_() { + let mut h = harness(blank()); + sequence(&mut h, "HOT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "sheet.tabColor"), "HOT must journal sheet.tabColor"); +} + +#[test] +fn keytip_houc_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.hideColumns"), "HOUC must journal home.hideColumns"); +} + +#[test] +fn keytip_houh_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUH", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOUH must invoke sheet.unhide and surface its missing-params error"); +} + +#[test] +fn keytip_houl_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.unhideColumns"), "HOUL must journal home.unhideColumns"); +} + +#[test] +fn keytip_houo_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.unhideRows"), "HOUO must journal home.unhideRows"); +} + +#[test] +fn keytip_hour_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.hideRows"), "HOUR must journal home.hideRows"); +} + +#[test] +fn keytip_hous_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOUS must invoke sheet.hide and surface its missing-params error"); +} + +#[test] +fn keytip_how_() { + let mut h = harness(blank()); + sequence(&mut h, "HOW", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOW must invoke home.columnWidth and surface its missing-params error"); +} + +#[test] +fn keytip_hp_() { + let mut h = harness(blank()); + sequence(&mut h, "HP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.percent"), "HP must journal home.percent"); +} + +#[test] +fn keytip_hsc_() { + let mut h = harness(blank()); + sequence(&mut h, "HSC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.clearFilter"), "HSC must journal data.clearFilter"); +} + +#[test] +fn keytip_hsf_() { + let mut h = harness(blank()); + sequence(&mut h, "HSF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.filter"), "HSF must journal data.filter"); +} + +#[test] +fn keytip_hso_() { + let mut h = harness(blank()); + sequence(&mut h, "HSO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortDescending"), "HSO must journal data.sortDescending"); +} + +#[test] +fn keytip_hss_() { + let mut h = harness(blank()); + sequence(&mut h, "HSS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortAscending"), "HSS must journal data.sortAscending"); +} + +#[test] +fn keytip_hsu_() { + let mut h = harness(blank()); + sequence(&mut h, "HSU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sort"), "HSU must journal data.sort"); +} + +#[test] +fn keytip_ht_() { + let mut h = harness(blank()); + sequence(&mut h, "HT", false); + assert_eq!(h.state().keytips.prefix(), Some("HT"), "HT must stay armed waiting for more input"); +} + +#[test] +fn keytip_htn_() { + let mut h = harness(blank()); + sequence(&mut h, "HTN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HTN must invoke table.style and surface its missing-params error"); +} + +#[test] +fn keytip_htp_() { + let mut h = harness(blank()); + sequence(&mut h, "HTP", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HTP must invoke table.style and surface its missing-params error"); +} + +#[test] +fn keytip_hu_() { + let mut h = harness(blank()); + sequence(&mut h, "HU", false); + assert_eq!(h.state().keytips.prefix(), Some("HU"), "HU must stay armed waiting for more input"); +} + +#[test] +fn keytip_hua_() { + let mut h = harness(blank()); + sequence(&mut h, "HUA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUA must journal formulas.autoSum"); +} + +#[test] +fn keytip_huc_() { + let mut h = harness(blank()); + sequence(&mut h, "HUC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUC must journal formulas.autoSum"); +} + +#[test] +fn keytip_hui_() { + let mut h = harness(blank()); + sequence(&mut h, "HUI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUI must journal formulas.autoSum"); +} + +#[test] +fn keytip_hum_() { + let mut h = harness(blank()); + sequence(&mut h, "HUM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUM must journal formulas.autoSum"); +} + +#[test] +fn keytip_hv_() { + let mut h = harness(blank()); + sequence(&mut h, "HV", false); + assert_eq!(h.state().keytips.prefix(), Some("HV"), "HV must stay armed waiting for more input"); +} + +#[test] +fn keytip_hva_() { + let mut h = harness(blank()); + sequence(&mut h, "HVA", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVA must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvb_() { + let mut h = harness(blank()); + sequence(&mut h, "HVB", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVB must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hve_() { + let mut h = harness(blank()); + sequence(&mut h, "HVE", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVE must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvf_() { + let mut h = harness(blank()); + sequence(&mut h, "HVF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVF must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvi_() { + let mut h = harness(blank()); + sequence(&mut h, "HVI", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVI must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvk_() { + let mut h = harness(blank()); + sequence(&mut h, "HVK", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVK must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvm_() { + let mut h = harness(blank()); + sequence(&mut h, "HVM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVM must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvn_() { + let mut h = harness(blank()); + sequence(&mut h, "HVN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVN must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvo_() { + let mut h = harness(blank()); + sequence(&mut h, "HVO", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVO must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvp_() { + let mut h = harness(blank()); + sequence(&mut h, "HVP", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVP must invoke edit.paste and surface its missing-params error"); +} + +#[test] +fn keytip_hvr_() { + let mut h = harness(blank()); + sequence(&mut h, "HVR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVR must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvt_() { + let mut h = harness(blank()); + sequence(&mut h, "HVT", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVT must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvu_() { + let mut h = harness(blank()); + sequence(&mut h, "HVU", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVU must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvv_() { + let mut h = harness(blank()); + sequence(&mut h, "HVV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVV must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvw_() { + let mut h = harness(blank()); + sequence(&mut h, "HVW", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVW must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hw_() { + let mut h = harness(blank()); + sequence(&mut h, "HW", false); + assert_eq!(h.state().keytips.prefix(), Some("HW"), "HW must stay armed waiting for more input"); +} + +#[test] +fn keytip_hws_() { + let mut h = harness(blank()); + sequence(&mut h, "HWS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.shrinkToFit"), "HWS must journal home.shrinkToFit"); +} + +#[test] +fn keytip_hww_() { + let mut h = harness(blank()); + sequence(&mut h, "HWW", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.wrapText"), "HWW must journal home.wrapText"); +} + +#[test] +fn keytip_hx_() { + let mut h = harness(blank()); + sequence(&mut h, "HX", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.cut"), "HX must journal edit.cut"); +} + +#[test] +fn keytip_nc1_() { + let mut h = harness(blank()); + sequence(&mut h, "NC1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NC1 must journal insert.chart"); +} + +#[test] +fn keytip_nc2_() { + let mut h = harness(blank()); + sequence(&mut h, "NC2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "NC2 must journal review.newComment"); +} + +#[test] +fn keytip_ncm_() { + let mut h = harness(blank()); + sequence(&mut h, "NCM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "NCM must journal review.newComment"); +} + +#[test] +fn keytip_ncb_() { + let mut h = harness(blank()); + sequence(&mut h, "NCB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.checkbox"), "NCB must journal insert.checkbox"); +} + +#[test] +fn keytip_nd_() { + let mut h = harness(blank()); + sequence(&mut h, "ND", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "ND must journal insert.chart"); +} + +#[test] +fn keytip_ne_() { + let mut h = harness(blank()); + sequence(&mut h, "NE", false); + assert!(h.state().dialog.is_none(), "NE must not open a dialog"); + assert!(h.state().message.is_none(), "NE must not open a message box"); + assert!(h.state().toast.is_none(), "NE must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NE must not journal"); +} + +#[test] +fn keytip_nf_() { + let mut h = harness(blank()); + sequence(&mut h, "NF", false); + assert_eq!(h.state().keytips.prefix(), Some("NF"), "NF must stay armed waiting for more input"); +} + +#[test] +fn keytip_nfp_() { + let mut h = harness(blank()); + sequence(&mut h, "NFP", false); + assert_eq!(h.state().keytips.prefix(), Some("NFP"), "NFP must stay armed waiting for more input"); +} + +#[test] +fn keytip_nfpf_() { + let mut h = harness(blank()); + sequence(&mut h, "NFPF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NFPF must journal insert.picture"); +} + +#[test] +fn keytip_nh1_() { + let mut h = harness(blank()); + sequence(&mut h, "NH1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.headerFooter"), "NH1 must journal pageLayout.headerFooter"); +} + +#[test] +fn keytip_nhf_() { + let mut h = harness(blank()); + sequence(&mut h, "NHF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.headerFooter"), "NHF must journal pageLayout.headerFooter"); +} + +#[test] +fn keytip_nhi_() { + let mut h = harness(blank()); + sequence(&mut h, "NHI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NHI must journal insert.chart"); +} + +#[test] +fn keytip_ni1_() { + let mut h = harness(blank()); + sequence(&mut h, "NI1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NI1 must journal insert.chart"); +} + +#[test] +fn keytip_ni2_() { + let mut h = harness(blank()); + sequence(&mut h, "NI2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.link"), "NI2 must journal insert.link"); +} + +#[test] +fn keytip_nj_() { + let mut h = harness(blank()); + sequence(&mut h, "NJ", false); + assert!(h.state().dialog.is_none(), "NJ must not open a dialog"); + assert!(h.state().message.is_none(), "NJ must not open a message box"); + assert!(h.state().toast.is_none(), "NJ must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NJ must not journal"); +} + +#[test] +fn keytip_nm1t_() { + let mut h = harness(blank()); + sequence(&mut h, "NM1T", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NM1T must journal insert.picture"); +} + +#[test] +fn keytip_nm2_() { + let mut h = harness(blank()); + sequence(&mut h, "NM2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NM2 must journal insert.chart"); +} + +#[test] +fn keytip_nn1_() { + let mut h = harness(blank()); + sequence(&mut h, "NN1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NN1 must journal insert.chart"); +} + +#[test] +fn keytip_nns_() { + let mut h = harness(blank()); + sequence(&mut h, "NNS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.icons"), "NNS must journal insert.icons"); +} + +#[test] +fn keytip_np_() { + let mut h = harness(blank()); + sequence(&mut h, "NP", false); + assert_eq!(h.state().keytips.prefix(), Some("NP"), "NP must stay armed waiting for more input"); +} + +#[test] +fn keytip_npb_() { + let mut h = harness(blank()); + sequence(&mut h, "NPB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPB must journal insert.picture"); +} + +#[test] +fn keytip_npf_() { + let mut h = harness(blank()); + sequence(&mut h, "NPF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPF must journal insert.picture"); +} + +#[test] +fn keytip_npi_() { + let mut h = harness(blank()); + sequence(&mut h, "NPI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPI must journal insert.picture"); +} + +#[test] +fn keytip_npo_() { + let mut h = harness(blank()); + sequence(&mut h, "NPO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPO must journal insert.picture"); +} + +#[test] +fn keytip_nq_() { + let mut h = harness(blank()); + sequence(&mut h, "NQ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NQ must journal insert.chart"); +} + +#[test] +fn keytip_nr_() { + let mut h = harness(blank()); + sequence(&mut h, "NR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.recommendedCharts"), "NR must journal insert.recommendedCharts"); +} + +#[test] +fn keytip_nsa_() { + let mut h = harness(blank()); + sequence(&mut h, "NSA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NSA must journal insert.chart"); +} + +#[test] +fn keytip_nsc_() { + let mut h = harness(blank()); + sequence(&mut h, "NSC", false); + assert!(h.state().dialog.is_none(), "NSC must not open a dialog"); + assert!(h.state().message.is_none(), "NSC must not open a message box"); + assert!(h.state().toast.is_none(), "NSC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NSC must not journal"); +} + +#[test] +fn keytip_nsd_() { + let mut h = harness(blank()); + sequence(&mut h, "NSD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NSD must journal insert.chart"); +} + +#[test] +fn keytip_nsf_() { + let mut h = harness(blank()); + sequence(&mut h, "NSF", false); + assert!(h.state().dialog.is_none(), "NSF must not open a dialog"); + assert!(h.state().message.is_none(), "NSF must not open a message box"); + assert!(h.state().toast.is_none(), "NSF must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NSF must not journal"); +} + +#[test] +fn keytip_nsh_() { + let mut h = harness(blank()); + sequence(&mut h, "NSH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.shape"), "NSH must journal insert.shape"); +} + +#[test] +fn keytip_nsp_() { + let mut h = harness(blank()); + sequence(&mut h, "NSP", false); + assert!( + h.state().message.is_some() || h.state().toast.is_some(), + "NSP must invoke insert.recommendedPivotTables and surface its missing-params error" + ); +} + +#[test] +fn keytip_nst_() { + let mut h = harness(blank()); + sequence(&mut h, "NST", false); + assert!(h.state().dialog.is_none(), "NST must not open a dialog"); + assert!(h.state().message.is_none(), "NST must not open a message box"); + assert!(h.state().toast.is_none(), "NST must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NST must not journal"); +} + +#[test] +fn keytip_nsz_() { + let mut h = harness(blank()); + sequence(&mut h, "NSZ", false); + assert!(h.state().dialog.is_none(), "NSZ must not open a dialog"); + assert!(h.state().message.is_none(), "NSZ must not open a message box"); + assert!(h.state().toast.is_none(), "NSZ must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NSZ must not journal"); +} + +#[test] +fn keytip_nt_() { + let mut h = harness(blank()); + sequence(&mut h, "NT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.table"), "NT must journal insert.table"); +} + +#[test] +fn keytip_nu_() { + let mut h = harness(blank()); + sequence(&mut h, "NU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.symbol"), "NU must journal insert.symbol"); +} + +#[test] +fn keytip_nzs_() { + let mut h = harness(blank()); + sequence(&mut h, "NZS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.symbol"), "NZS must journal insert.symbol"); +} + +#[test] +fn keytip_nv_() { + let mut h = harness(blank()); + sequence(&mut h, "NV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NV must invoke insert.pivotTable and surface its missing-params error"); +} + +#[test] +fn keytip_nw_() { + let mut h = harness(blank()); + sequence(&mut h, "NW", false); + assert!(h.state().dialog.is_none(), "NW must not open a dialog"); + assert!(h.state().message.is_none(), "NW must not open a message box"); + assert!(h.state().toast.is_none(), "NW must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NW must not journal"); +} + +#[test] +fn keytip_nx_() { + let mut h = harness(blank()); + sequence(&mut h, "NX", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.textBox"), "NX must journal insert.textBox"); +} + +#[test] +fn keytip_nzk_() { + let mut h = harness(blank()); + sequence(&mut h, "NZK", false); + assert_eq!(h.state().keytips.prefix(), Some("NZK"), "NZK must stay armed waiting for more input"); +} + +#[test] +fn keytip_nzksl_() { + let mut h = harness(blank()); + sequence(&mut h, "NZKSL", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSL must invoke insert.sparkline and surface its missing-params error"); +} + +#[test] +fn keytip_nzkso_() { + let mut h = harness(blank()); + sequence(&mut h, "NZKSO", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSO must invoke insert.sparkline and surface its missing-params error"); +} + +#[test] +fn keytip_nzksw_() { + let mut h = harness(blank()); + sequence(&mut h, "NZKSW", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSW must invoke insert.sparkline and surface its missing-params error"); +} + +#[test] +fn keytip_nztx_() { + let mut h = harness(blank()); + sequence(&mut h, "NZTX", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.textBox"), "NZTX must journal insert.textBox"); +} + +#[test] +fn keytip_nzt_() { + let mut h = harness(blank()); + sequence(&mut h, "NZT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.textBox"), "NZT must journal insert.textBox"); +} + +#[test] +fn keytip_pae_() { + let mut h = harness(blank()); + sequence(&mut h, "PAE", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "PAE must invoke arrange.sendBackward and surface its missing-params error"); +} + +#[test] +fn keytip_paf_() { + let mut h = harness(blank()); + sequence(&mut h, "PAF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "PAF must invoke arrange.bringForward and surface its missing-params error"); +} + +#[test] +fn keytip_pb_() { + let mut h = harness(blank()); + sequence(&mut h, "PB", false); + assert_eq!(h.state().keytips.prefix(), Some("PB"), "PB must stay armed waiting for more input"); +} + +#[test] +fn keytip_pbi_() { + let mut h = harness(blank()); + sequence(&mut h, "PBI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.breaks"), "PBI must journal pageLayout.breaks"); +} + +#[test] +fn keytip_pbr_() { + let mut h = harness(blank()); + sequence(&mut h, "PBR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.breaks"), "PBR must journal pageLayout.breaks"); +} + +#[test] +fn keytip_pg_() { + let mut h = harness(blank()); + sequence(&mut h, "PG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.background"), "PG must journal pageLayout.background"); +} + +#[test] +fn keytip_pi_() { + let mut h = harness(blank()); + sequence(&mut h, "PI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printTitles"), "PI must journal pageLayout.printTitles"); +} + +#[test] +fn keytip_pm_() { + let mut h = harness(blank()); + sequence(&mut h, "PM", false); + assert_eq!(h.state().keytips.prefix(), Some("PM"), "PM must stay armed waiting for more input"); +} + +#[test] +fn keytip_pma_() { + let mut h = harness(blank()); + sequence(&mut h, "PMA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.margins"), "PMA must journal pageLayout.margins"); +} + +#[test] +fn keytip_po_() { + let mut h = harness(blank()); + sequence(&mut h, "PO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.orientation"), "PO must journal pageLayout.orientation"); +} + +#[test] +fn keytip_ppg_() { + let mut h = harness(blank()); + sequence(&mut h, "PPG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printGridlines"), "PPG must journal pageLayout.printGridlines"); +} + +#[test] +fn keytip_pph_() { + let mut h = harness(blank()); + sequence(&mut h, "PPH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printHeadings"), "PPH must journal pageLayout.printHeadings"); +} + +#[test] +fn keytip_pr_() { + let mut h = harness(blank()); + sequence(&mut h, "PR", false); + assert_eq!(h.state().keytips.prefix(), Some("PR"), "PR must stay armed waiting for more input"); +} + +#[test] +fn keytip_prs_() { + let mut h = harness(blank()); + sequence(&mut h, "PRS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printArea"), "PRS must journal pageLayout.printArea"); +} + +#[test] +fn keytip_psp_() { + let mut h = harness(blank()); + sequence(&mut h, "PSP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.pageSetup"), "PSP must journal pageLayout.pageSetup"); +} + +#[test] +fn keytip_psz_() { + let mut h = harness(blank()); + sequence(&mut h, "PSZ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.size"), "PSZ must journal pageLayout.size"); +} + +#[test] +fn keytip_ptc_() { + let mut h = harness(blank()); + sequence(&mut h, "PTC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.themeColors"), "PTC must journal pageLayout.themeColors"); +} + +#[test] +fn keytip_pte_() { + let mut h = harness(blank()); + sequence(&mut h, "PTE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.themeEffects"), "PTE must journal pageLayout.themeEffects"); +} + +#[test] +fn keytip_ptf_() { + let mut h = harness(blank()); + sequence(&mut h, "PTF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.themeFonts"), "PTF must journal pageLayout.themeFonts"); +} + +#[test] +fn keytip_pth_() { + let mut h = harness(blank()); + sequence(&mut h, "PTH", false); + assert_eq!(h.state().keytips.prefix(), Some("PTH"), "PTH must stay armed waiting for more input"); +} + +#[test] +fn keytip_pthb_() { + let mut h = harness(blank()); + sequence(&mut h, "PTHB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.theme"), "PTHB must journal pageLayout.theme"); +} + +#[test] +fn keytip_pvg_() { + let mut h = harness(blank()); + sequence(&mut h, "PVG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines"), "PVG must journal view.gridlines"); +} + +#[test] +fn keytip_pvh_() { + let mut h = harness(blank()); + sequence(&mut h, "PVH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.headings"), "PVH must journal view.headings"); +} + +#[test] +fn keytip_ma_() { + let mut h = harness(blank()); + sequence(&mut h, "MA", false); + assert_eq!(h.state().keytips.prefix(), Some("MA"), "MA must stay armed waiting for more input"); +} + +#[test] +fn keytip_maa_() { + let mut h = harness(blank()); + sequence(&mut h, "MAA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.removeArrows"), "MAA must journal formulas.removeArrows"); +} + +#[test] +fn keytip_map_() { + let mut h = harness(blank()); + sequence(&mut h, "MAP", false); + assert!(h.state().dialog.is_none(), "MAP must not open a dialog"); + assert!(h.state().message.is_none(), "MAP must not open a message box"); + assert!(h.state().toast.is_none(), "MAP must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "MAP must not journal"); +} + +#[test] +fn keytip_mb_() { + let mut h = harness(blank()); + sequence(&mut h, "MB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculateNow"), "MB must journal formulas.calculateNow"); +} + +#[test] +fn keytip_mc_() { + let mut h = harness(blank()); + sequence(&mut h, "MC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.createFromSelection"), "MC must journal formulas.createFromSelection"); +} + +#[test] +fn keytip_md_() { + let mut h = harness(blank()); + sequence(&mut h, "MD", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MD must run formulas.traceDependents cleanly"); +} + +#[test] +fn keytip_me_() { + let mut h = harness(blank()); + sequence(&mut h, "ME", false); + assert_eq!(h.state().keytips.prefix(), Some("ME"), "ME must stay armed waiting for more input"); +} + +#[test] +fn keytip_mef_() { + let mut h = harness(blank()); + sequence(&mut h, "MEF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MEF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mf_() { + let mut h = harness(blank()); + sequence(&mut h, "MF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mg_() { + let mut h = harness(blank()); + sequence(&mut h, "MG", false); + assert_eq!(h.state().keytips.prefix(), Some("MG"), "MG must stay armed waiting for more input"); +} + +#[test] +fn keytip_mgf_() { + let mut h = harness(blank()); + sequence(&mut h, "MGF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MGF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mh_() { + let mut h = harness(blank()); + sequence(&mut h, "MH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.showFormulas"), "MH must journal formulas.showFormulas"); +} + +#[test] +fn keytip_mi_() { + let mut h = harness(blank()); + sequence(&mut h, "MI", false); + assert_eq!(h.state().keytips.prefix(), Some("MI"), "MI must stay armed waiting for more input"); +} + +#[test] +fn keytip_mi1_() { + let mut h = harness(blank()); + sequence(&mut h, "MI1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MI1 must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mif_() { + let mut h = harness(blank()); + sequence(&mut h, "MIF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MIF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mj_() { + let mut h = harness(blank()); + sequence(&mut h, "MJ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculateSheet"), "MJ must journal formulas.calculateSheet"); +} + +#[test] +fn keytip_mk_() { + let mut h = harness(blank()); + sequence(&mut h, "MK", false); + assert_eq!(h.state().keytips.prefix(), Some("MK"), "MK must stay armed waiting for more input"); +} + +#[test] +fn keytip_mkk_() { + let mut h = harness(blank()); + sequence(&mut h, "MKK", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MKK must run formulas.errorChecking cleanly"); +} + +#[test] +fn keytip_ml_() { + let mut h = harness(blank()); + sequence(&mut h, "ML", false); + assert_eq!(h.state().keytips.prefix(), Some("ML"), "ML must stay armed waiting for more input"); +} + +#[test] +fn keytip_mlf_() { + let mut h = harness(blank()); + sequence(&mut h, "MLF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MLF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mm_() { + let mut h = harness(blank()); + sequence(&mut h, "MM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.defineName"), "MM must journal formulas.defineName"); +} + +#[test] +fn keytip_mn_() { + let mut h = harness(blank()); + sequence(&mut h, "MN", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MN must run formulas.nameManager cleanly"); +} + +#[test] +fn keytip_mo_() { + let mut h = harness(blank()); + sequence(&mut h, "MO", false); + assert_eq!(h.state().keytips.prefix(), Some("MO"), "MO must stay armed waiting for more input"); +} + +#[test] +fn keytip_mof_() { + let mut h = harness(blank()); + sequence(&mut h, "MOF", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MOF must run formulas.lookupReference cleanly"); +} + +#[test] +fn keytip_mp_() { + let mut h = harness(blank()); + sequence(&mut h, "MP", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MP must run formulas.tracePrecedents cleanly"); +} + +#[test] +fn keytip_mq_() { + let mut h = harness(blank()); + sequence(&mut h, "MQ", false); + assert_eq!(h.state().keytips.prefix(), Some("MQ"), "MQ must stay armed waiting for more input"); +} + +#[test] +fn keytip_mqcf_() { + let mut h = harness(blank()); + sequence(&mut h, "MQCF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQCF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqef_() { + let mut h = harness(blank()); + sequence(&mut h, "MQEF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQEF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqif_() { + let mut h = harness(blank()); + sequence(&mut h, "MQIF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQIF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqof_() { + let mut h = harness(blank()); + sequence(&mut h, "MQOF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQOF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqsf_() { + let mut h = harness(blank()); + sequence(&mut h, "MQSF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQSF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqwf_() { + let mut h = harness(blank()); + sequence(&mut h, "MQWF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQWF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mr_() { + let mut h = harness(blank()); + sequence(&mut h, "MR", false); + assert_eq!(h.state().keytips.prefix(), Some("MR"), "MR must stay armed waiting for more input"); +} + +#[test] +fn keytip_mrf_() { + let mut h = harness(blank()); + sequence(&mut h, "MRF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MRF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_ms_() { + let mut h = harness(blank()); + sequence(&mut h, "MS", false); + assert_eq!(h.state().keytips.prefix(), Some("MS"), "MS must stay armed waiting for more input"); +} + +#[test] +fn keytip_msp_() { + let mut h = harness(blank()); + sequence(&mut h, "MSP", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "MSP must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_mt_() { + let mut h = harness(blank()); + sequence(&mut h, "MT", false); + assert_eq!(h.state().keytips.prefix(), Some("MT"), "MT must stay armed waiting for more input"); +} + +#[test] +fn keytip_mtf_() { + let mut h = harness(blank()); + sequence(&mut h, "MTF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MTF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mu_() { + let mut h = harness(blank()); + sequence(&mut h, "MU", false); + assert_eq!(h.state().keytips.prefix(), Some("MU"), "MU must stay armed waiting for more input"); +} + +#[test] +fn keytip_mua_() { + let mut h = harness(blank()); + sequence(&mut h, "MUA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUA must journal formulas.autoSum"); +} + +#[test] +fn keytip_muc_() { + let mut h = harness(blank()); + sequence(&mut h, "MUC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUC must journal formulas.autoSum"); +} + +#[test] +fn keytip_muf_() { + let mut h = harness(blank()); + sequence(&mut h, "MUF", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MUF must run formulas.moreFunctions cleanly"); +} + +#[test] +fn keytip_mui_() { + let mut h = harness(blank()); + sequence(&mut h, "MUI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUI must journal formulas.autoSum"); +} + +#[test] +fn keytip_mum_() { + let mut h = harness(blank()); + sequence(&mut h, "MUM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUM must journal formulas.autoSum"); +} + +#[test] +fn keytip_mus_() { + let mut h = harness(blank()); + sequence(&mut h, "MUS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUS must journal formulas.autoSum"); +} + +#[test] +fn keytip_mv_() { + let mut h = harness(blank()); + sequence(&mut h, "MV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "MV must invoke formulas.evaluateFormula and surface its missing-params error"); +} + +#[test] +fn keytip_mw_() { + let mut h = harness(blank()); + sequence(&mut h, "MW", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MW must run formulas.watchWindow cleanly"); +} + +#[test] +fn keytip_mx_() { + let mut h = harness(blank()); + sequence(&mut h, "MX", false); + assert_eq!(h.state().keytips.prefix(), Some("MX"), "MX must stay armed waiting for more input"); +} + +#[test] +fn keytip_mxa_() { + let mut h = harness(blank()); + sequence(&mut h, "MXA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculationOptions"), "MXA must journal formulas.calculationOptions"); +} + +#[test] +fn keytip_mxm_() { + let mut h = harness(blank()); + sequence(&mut h, "MXM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculationOptions"), "MXM must journal formulas.calculationOptions"); +} + +#[test] +fn keytip_mxp_() { + let mut h = harness(blank()); + sequence(&mut h, "MXP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculationOptions"), "MXP must journal formulas.calculationOptions"); +} + +#[test] +fn keytip_ab_() { + let mut h = harness(blank()); + sequence(&mut h, "AB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.subtotal"), "AB must journal data.subtotal"); +} + +#[test] +fn keytip_ac_() { + let mut h = harness(blank()); + sequence(&mut h, "AC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.clearFilter"), "AC must journal data.clearFilter"); +} + +#[test] +fn keytip_ad_() { + let mut h = harness(blank()); + sequence(&mut h, "AD", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AD must invoke data.consolidate and surface its missing-params error"); +} + +#[test] +fn keytip_ae_() { + let mut h = harness(blank()); + sequence(&mut h, "AE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.textToColumns"), "AE must journal data.textToColumns"); +} + +#[test] +fn keytip_af1_() { + let mut h = harness(blank()); + sequence(&mut h, "AF1", false); + assert_eq!(h.state().keytips.prefix(), Some("AF1"), "AF1 must stay armed waiting for more input"); +} + +#[test] +fn keytip_af1c_() { + let mut h = harness(blank()); + sequence(&mut h, "AF1C", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "AF1C must journal insert.picture"); +} + +#[test] +fn keytip_af1p_() { + let mut h = harness(blank()); + sequence(&mut h, "AF1P", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "AF1P must journal insert.picture"); +} + +#[test] +fn keytip_afa_() { + let mut h = harness(blank()); + sequence(&mut h, "AFA", false); + assert!(h.state().dialog.is_none(), "AFA must not open a dialog"); + assert!(h.state().message.is_none(), "AFA must not open a message box"); + assert!(h.state().toast.is_none(), "AFA must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFA must not journal"); +} + +#[test] +fn keytip_afc_() { + let mut h = harness(blank()); + sequence(&mut h, "AFC", false); + assert!(h.state().dialog.is_none(), "AFC must not open a dialog"); + assert!(h.state().message.is_none(), "AFC must not open a message box"); + assert!(h.state().toast.is_none(), "AFC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFC must not journal"); +} + +#[test] +fn keytip_afe_() { + let mut h = harness(blank()); + sequence(&mut h, "AFE", false); + assert!(h.state().dialog.is_none(), "AFE must not open a dialog"); + assert!(h.state().message.is_none(), "AFE must not open a message box"); + assert!(h.state().toast.is_none(), "AFE must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFE must not journal"); +} + +#[test] +fn keytip_aff_() { + let mut h = harness(blank()); + sequence(&mut h, "AFF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AFF must invoke edit.flashFill and surface its missing-params error"); +} + +#[test] +fn keytip_afo_() { + let mut h = harness(blank()); + sequence(&mut h, "AFO", false); + assert!(h.state().dialog.is_none(), "AFO must not open a dialog"); + assert!(h.state().message.is_none(), "AFO must not open a message box"); + assert!(h.state().toast.is_none(), "AFO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFO must not journal"); +} + +#[test] +fn keytip_aft_() { + let mut h = harness(blank()); + sequence(&mut h, "AFT", false); + assert!(h.state().dialog.is_none(), "AFT must not open a dialog"); + assert!(h.state().message.is_none(), "AFT must not open a message box"); + assert!(h.state().toast.is_none(), "AFT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFT must not journal"); +} + +#[test] +fn keytip_afw_() { + let mut h = harness(blank()); + sequence(&mut h, "AFW", false); + assert!(h.state().dialog.is_none(), "AFW must not open a dialog"); + assert!(h.state().message.is_none(), "AFW must not open a message box"); + assert!(h.state().toast.is_none(), "AFW must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFW must not journal"); +} + +#[test] +fn keytip_ag_() { + let mut h = harness(blank()); + sequence(&mut h, "AG", false); + assert_eq!(h.state().keytips.prefix(), Some("AG"), "AG must stay armed waiting for more input"); +} + +#[test] +fn keytip_aga_() { + let mut h = harness(blank()); + sequence(&mut h, "AGA", false); + assert!(h.state().dialog.is_none(), "AGA must not open a dialog"); + assert!(h.state().message.is_none(), "AGA must not open a message box"); + assert!(h.state().toast.is_none(), "AGA must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AGA must not journal"); +} + +#[test] +fn keytip_agg_() { + let mut h = harness(blank()); + sequence(&mut h, "AGG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.group"), "AGG must journal data.group"); +} + +#[test] +fn keytip_ah_() { + let mut h = harness(blank()); + sequence(&mut h, "AH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.hideDetail"), "AH must journal data.hideDetail"); +} + +#[test] +fn keytip_aj_() { + let mut h = harness(blank()); + sequence(&mut h, "AJ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.showDetail"), "AJ must journal data.showDetail"); +} + +#[test] +fn keytip_ak_() { + let mut h = harness(blank()); + sequence(&mut h, "AK", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "AK must run data.queriesConnections cleanly"); +} + +#[test] +fn keytip_am_() { + let mut h = harness(blank()); + sequence(&mut h, "AM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.removeDuplicates"), "AM must journal data.removeDuplicates"); +} + +#[test] +fn keytip_ao_() { + let mut h = harness(blank()); + sequence(&mut h, "AO", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "AO must run data.queriesConnections cleanly"); +} + +#[test] +fn keytip_ap1_() { + let mut h = harness(blank()); + sequence(&mut h, "AP1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.properties"), "AP1 must journal file.properties"); +} + +#[test] +fn keytip_apn_() { + let mut h = harness(blank()); + sequence(&mut h, "APN", false); + assert_eq!(h.state().keytips.prefix(), Some("APN"), "APN must stay armed waiting for more input"); +} + +#[test] +fn keytip_apnd_() { + let mut h = harness(blank()); + sequence(&mut h, "APND", false); + assert!(h.state().dialog.is_none(), "APND must not open a dialog"); + assert!(h.state().message.is_none(), "APND must not open a message box"); + assert!(h.state().toast.is_none(), "APND must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APND must not journal"); +} + +#[test] +fn keytip_apne_() { + let mut h = harness(blank()); + sequence(&mut h, "APNE", false); + assert!(h.state().dialog.is_none(), "APNE must not open a dialog"); + assert!(h.state().message.is_none(), "APNE must not open a message box"); + assert!(h.state().toast.is_none(), "APNE must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNE must not journal"); +} + +#[test] +fn keytip_apng_() { + let mut h = harness(blank()); + sequence(&mut h, "APNG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.getData"), "APNG must journal data.getData"); +} + +#[test] +fn keytip_apnh_() { + let mut h = harness(blank()); + sequence(&mut h, "APNH", false); + assert!(h.state().dialog.is_none(), "APNH must not open a dialog"); + assert!(h.state().message.is_none(), "APNH must not open a message box"); + assert!(h.state().toast.is_none(), "APNH must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNH must not journal"); +} + +#[test] +fn keytip_apnl_() { + let mut h = harness(blank()); + sequence(&mut h, "APNL", false); + assert!(h.state().dialog.is_none(), "APNL must not open a dialog"); + assert!(h.state().message.is_none(), "APNL must not open a message box"); + assert!(h.state().toast.is_none(), "APNL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNL must not journal"); +} + +#[test] +fn keytip_apns_() { + let mut h = harness(blank()); + sequence(&mut h, "APNS", false); + assert!(h.state().dialog.is_none(), "APNS must not open a dialog"); + assert!(h.state().message.is_none(), "APNS must not open a message box"); + assert!(h.state().toast.is_none(), "APNS must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNS must not journal"); +} + +#[test] +fn keytip_apnt_() { + let mut h = harness(blank()); + sequence(&mut h, "APNT", false); + assert!(h.state().dialog.is_none(), "APNT must not open a dialog"); + assert!(h.state().message.is_none(), "APNT must not open a message box"); + assert!(h.state().toast.is_none(), "APNT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNT must not journal"); +} + +#[test] +fn keytip_apr_() { + let mut h = harness(blank()); + sequence(&mut h, "APR", false); + assert!(h.state().dialog.is_none(), "APR must not open a dialog"); + assert!(h.state().message.is_none(), "APR must not open a message box"); + assert!(h.state().toast.is_none(), "APR must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APR must not journal"); +} + +#[test] +fn keytip_aps_() { + let mut h = harness(blank()); + sequence(&mut h, "APS", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "APS must run data.queriesConnections cleanly"); +} + +#[test] +fn keytip_apt_() { + let mut h = harness(blank()); + sequence(&mut h, "APT", false); + assert!(h.state().dialog.is_none(), "APT must not open a dialog"); + assert!(h.state().message.is_none(), "APT must not open a message box"); + assert!(h.state().toast.is_none(), "APT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APT must not journal"); +} + +#[test] +fn keytip_aq_() { + let mut h = harness(blank()); + sequence(&mut h, "AQ", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AQ must invoke data.advancedFilter and surface its missing-params error"); +} + +#[test] +fn keytip_ar_() { + let mut h = harness(blank()); + sequence(&mut h, "AR", false); + assert_eq!(h.state().keytips.prefix(), Some("AR"), "AR must stay armed waiting for more input"); +} + +#[test] +fn keytip_ara_() { + let mut h = harness(blank()); + sequence(&mut h, "ARA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.refreshAll"), "ARA must journal data.refreshAll"); +} + +#[test] +fn keytip_arc_() { + let mut h = harness(blank()); + sequence(&mut h, "ARC", false); + assert!(h.state().dialog.is_none(), "ARC must not open a dialog"); + assert!(h.state().message.is_none(), "ARC must not open a message box"); + assert!(h.state().toast.is_none(), "ARC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "ARC must not journal"); +} + +#[test] +fn keytip_aro_() { + let mut h = harness(blank()); + sequence(&mut h, "ARO", false); + assert!(h.state().dialog.is_none(), "ARO must not open a dialog"); + assert!(h.state().message.is_none(), "ARO must not open a message box"); + assert!(h.state().toast.is_none(), "ARO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "ARO must not journal"); +} + +#[test] +fn keytip_arr_() { + let mut h = harness(blank()); + sequence(&mut h, "ARR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "ARR must invoke pivot.refresh and surface its missing-params error"); +} + +#[test] +fn keytip_ars_() { + let mut h = harness(blank()); + sequence(&mut h, "ARS", false); + assert!(h.state().dialog.is_none(), "ARS must not open a dialog"); + assert!(h.state().message.is_none(), "ARS must not open a message box"); + assert!(h.state().toast.is_none(), "ARS must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "ARS must not journal"); +} + +#[test] +fn keytip_asa_() { + let mut h = harness(blank()); + sequence(&mut h, "ASA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortAscending"), "ASA must journal data.sortAscending"); +} + +#[test] +fn keytip_asd_() { + let mut h = harness(blank()); + sequence(&mut h, "ASD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortDescending"), "ASD must journal data.sortDescending"); +} + +#[test] +fn keytip_ass_() { + let mut h = harness(blank()); + sequence(&mut h, "ASS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sort"), "ASS must journal data.sort"); +} + +#[test] +fn keytip_at_() { + let mut h = harness(blank()); + sequence(&mut h, "AT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.filter"), "AT must journal data.filter"); +} + +#[test] +fn keytip_au_() { + let mut h = harness(blank()); + sequence(&mut h, "AU", false); + assert_eq!(h.state().keytips.prefix(), Some("AU"), "AU must stay armed waiting for more input"); +} + +#[test] +fn keytip_auc_() { + let mut h = harness(blank()); + sequence(&mut h, "AUC", false); + assert!(h.state().dialog.is_none(), "AUC must not open a dialog"); + assert!(h.state().message.is_none(), "AUC must not open a message box"); + assert!(h.state().toast.is_none(), "AUC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AUC must not journal"); +} + +#[test] +fn keytip_auu_() { + let mut h = harness(blank()); + sequence(&mut h, "AUU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.ungroup"), "AUU must journal data.ungroup"); +} + +#[test] +fn keytip_av_() { + let mut h = harness(blank()); + sequence(&mut h, "AV", false); + assert_eq!(h.state().keytips.prefix(), Some("AV"), "AV must stay armed waiting for more input"); +} + +#[test] +fn keytip_avi_() { + let mut h = harness(blank()); + sequence(&mut h, "AVI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.circleInvalid"), "AVI must journal data.circleInvalid"); +} + +#[test] +fn keytip_avr_() { + let mut h = harness(blank()); + sequence(&mut h, "AVR", false); + assert!(h.state().dialog.is_none(), "AVR must not open a dialog"); + assert!(h.state().message.is_none(), "AVR must not open a message box"); + assert!(h.state().toast.is_none(), "AVR must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AVR must not journal"); +} + +#[test] +fn keytip_avv_() { + let mut h = harness(blank()); + sequence(&mut h, "AVV", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.validation"), "AVV must journal data.validation"); +} + +#[test] +fn keytip_aw_() { + let mut h = harness(blank()); + sequence(&mut h, "AW", false); + assert_eq!(h.state().keytips.prefix(), Some("AW"), "AW must stay armed waiting for more input"); +} + +#[test] +fn keytip_awg_() { + let mut h = harness(blank()); + sequence(&mut h, "AWG", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AWG must invoke data.goalSeek and surface its missing-params error"); +} + +#[test] +fn keytip_aws_() { + let mut h = harness(blank()); + sequence(&mut h, "AWS", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "AWS must run data.scenarioManager cleanly"); +} + +#[test] +fn keytip_awt_() { + let mut h = harness(blank()); + sequence(&mut h, "AWT", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AWT must invoke data.dataTable and surface its missing-params error"); +} + +#[test] +fn keytip_ax_() { + let mut h = harness(blank()); + sequence(&mut h, "AX", false); + assert!(h.state().dialog.is_none(), "AX must not open a dialog"); + assert!(h.state().message.is_none(), "AX must not open a message box"); + assert!(h.state().toast.is_none(), "AX must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AX must not journal"); +} + +#[test] +fn keytip_ay_() { + let mut h = harness(blank()); + sequence(&mut h, "AY", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.reapply"), "AY must journal data.reapply"); +} + +#[test] +fn keytip_ra1_() { + let mut h = harness(blank()); + sequence(&mut h, "RA1", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "RA1 must run review.checkAccessibility cleanly"); +} + +#[test] +fn keytip_ra2_() { + let mut h = harness(blank()); + sequence(&mut h, "RA2", false); + assert!(h.state().dialog.is_none(), "RA2 must not open a dialog"); + assert!(h.state().message.is_none(), "RA2 must not open a message box"); + assert!(h.state().toast.is_none(), "RA2 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RA2 must not journal"); +} + +#[test] +fn keytip_rc_() { + let mut h = harness(blank()); + sequence(&mut h, "RC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "RC must journal review.newComment"); +} + +#[test] +fn keytip_rd_() { + let mut h = harness(blank()); + sequence(&mut h, "RD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.deleteComment"), "RD must journal review.deleteComment"); +} + +#[test] +fn keytip_rg_() { + let mut h = harness(blank()); + sequence(&mut h, "RG", false); + assert!(h.state().dialog.is_none(), "RG must not open a dialog"); + assert!(h.state().message.is_none(), "RG must not open a message box"); + assert!(h.state().toast.is_none(), "RG must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RG must not journal"); +} + +#[test] +fn keytip_rl_() { + let mut h = harness(blank()); + sequence(&mut h, "RL", false); + assert!(h.state().dialog.is_none(), "RL must not open a dialog"); + assert!(h.state().message.is_none(), "RL must not open a message box"); + assert!(h.state().toast.is_none(), "RL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RL must not journal"); +} + +#[test] +fn keytip_rn_() { + let mut h = harness(blank()); + sequence(&mut h, "RN", false); + assert!(h.state().dialog.is_none(), "RN must not open a dialog"); + assert!(h.state().message.is_none(), "RN must not open a message box"); + assert!(h.state().toast.is_none(), "RN must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RN must not journal"); +} + +#[test] +fn keytip_ro_() { + let mut h = harness(blank()); + sequence(&mut h, "RO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectWorkbook"), "RO must journal review.protectWorkbook"); +} + +#[test] +fn keytip_rps_() { + let mut h = harness(blank()); + sequence(&mut h, "RPS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectSheet"), "RPS must journal review.protectSheet"); +} + +#[test] +fn keytip_rpw_() { + let mut h = harness(blank()); + sequence(&mut h, "RPW", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectWorkbook"), "RPW must journal review.protectWorkbook"); +} + +#[test] +fn keytip_rrs_() { + let mut h = harness(blank()); + sequence(&mut h, "RRS", false); + assert!(h.state().dialog.is_none(), "RRS must not open a dialog"); + assert!(h.state().message.is_none(), "RRS must not open a message box"); + assert!(h.state().toast.is_none(), "RRS must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RRS must not journal"); +} + +#[test] +fn keytip_rs_() { + let mut h = harness(blank()); + sequence(&mut h, "RS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "RS must invoke review.spelling and surface its missing-params error"); +} + +#[test] +fn keytip_rtn_() { + let mut h = harness(blank()); + sequence(&mut h, "RTN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newNote"), "RTN must journal review.newNote"); +} + +#[test] +fn keytip_rv_() { + let mut h = harness(blank()); + sequence(&mut h, "RV", false); + assert!(h.state().dialog.is_none(), "RV must not open a dialog"); + assert!(h.state().message.is_none(), "RV must not open a message box"); + assert!(h.state().toast.is_none(), "RV must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RV must not journal"); +} + +#[test] +fn keytip_rw_() { + let mut h = harness(blank()); + sequence(&mut h, "RW", false); + assert!(h.state().dialog.is_none(), "RW must not open a dialog"); + assert!(h.state().message.is_none(), "RW must not open a message box"); + assert!(h.state().toast.is_none(), "RW must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RW must not journal"); +} + +#[test] +fn keytip_wa_() { + let mut h = harness(blank()); + sequence(&mut h, "WA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.arrangeAll"), "WA must journal view.arrangeAll"); +} + +#[test] +fn keytip_wb_() { + let mut h = harness(blank()); + sequence(&mut h, "WB", false); + assert!(h.state().dialog.is_none(), "WB must not open a dialog"); + assert!(h.state().message.is_none(), "WB must not open a message box"); + assert!(h.state().toast.is_none(), "WB must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WB must not journal"); +} + +#[test] +fn keytip_wc_() { + let mut h = harness(blank()); + sequence(&mut h, "WC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.customViews"), "WC must journal view.customViews"); +} + +#[test] +fn keytip_wf_() { + let mut h = harness(blank()); + sequence(&mut h, "WF", false); + assert_eq!(h.state().keytips.prefix(), Some("WF"), "WF must stay armed waiting for more input"); +} + +#[test] +fn keytip_wfc_() { + let mut h = harness(blank()); + sequence(&mut h, "WFC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.freezeFirstColumn"), "WFC must journal view.freezeFirstColumn"); +} + +#[test] +fn keytip_wff_() { + let mut h = harness(blank()); + sequence(&mut h, "WFF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.freezePanes"), "WFF must journal view.freezePanes"); +} + +#[test] +fn keytip_wfr_() { + let mut h = harness(blank()); + sequence(&mut h, "WFR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.freezeTopRow"), "WFR must journal view.freezeTopRow"); +} + +#[test] +fn keytip_wg_() { + let mut h = harness(blank()); + sequence(&mut h, "WG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.zoomToSelection"), "WG must journal view.zoomToSelection"); +} + +#[test] +fn keytip_wh_() { + let mut h = harness(blank()); + sequence(&mut h, "WH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.hideWindow"), "WH must journal view.hideWindow"); +} + +#[test] +fn keytip_wi_() { + let mut h = harness(blank()); + sequence(&mut h, "WI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.pageBreakPreview"), "WI must journal view.pageBreakPreview"); +} + +#[test] +fn keytip_wj_() { + let mut h = harness(blank()); + sequence(&mut h, "WJ", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "WJ must invoke view.zoom and surface its missing-params error"); +} + +#[test] +fn keytip_wl_() { + let mut h = harness(blank()); + sequence(&mut h, "WL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.normal"), "WL must journal view.normal"); +} + +#[test] +fn keytip_wm_() { + let mut h = harness(blank()); + sequence(&mut h, "WM", false); + assert_eq!(h.state().keytips.prefix(), Some("WM"), "WM must stay armed waiting for more input"); +} + +#[test] +fn keytip_wmr_() { + let mut h = harness(blank()); + sequence(&mut h, "WMR", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WMR must run view.macros cleanly"); +} + +#[test] +fn keytip_wmv_() { + let mut h = harness(blank()); + sequence(&mut h, "WMV", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WMV must run view.macros cleanly"); +} + +#[test] +fn keytip_wn_() { + let mut h = harness(blank()); + sequence(&mut h, "WN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.newWindow"), "WN must journal view.newWindow"); +} + +#[test] +fn keytip_wp_() { + let mut h = harness(blank()); + sequence(&mut h, "WP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.pageLayout"), "WP must journal view.pageLayout"); +} + +#[test] +fn keytip_wq_() { + let mut h = harness(blank()); + sequence(&mut h, "WQ", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "WQ must invoke view.zoom and surface its missing-params error"); +} + +#[test] +fn keytip_wr_() { + let mut h = harness(blank()); + sequence(&mut h, "WR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.ruler"), "WR must journal view.ruler"); +} + +#[test] +fn keytip_ws_() { + let mut h = harness(blank()); + sequence(&mut h, "WS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.split"), "WS must journal view.split"); +} + +#[test] +fn keytip_wt_() { + let mut h = harness(blank()); + sequence(&mut h, "WT", false); + assert!(h.state().dialog.is_none(), "WT must not open a dialog"); + assert!(h.state().message.is_none(), "WT must not open a message box"); + assert!(h.state().toast.is_none(), "WT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WT must not journal"); +} + +#[test] +fn keytip_wu_() { + let mut h = harness(blank()); + sequence(&mut h, "WU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.unhideWindow"), "WU must journal view.unhideWindow"); +} + +#[test] +fn keytip_wvc_() { + let mut h = harness(blank()); + sequence(&mut h, "WVC", false); + assert!(h.state().dialog.is_none(), "WVC must not open a dialog"); + assert!(h.state().message.is_none(), "WVC must not open a message box"); + assert!(h.state().toast.is_none(), "WVC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVC must not journal"); +} + +#[test] +fn keytip_wvf_() { + let mut h = harness(blank()); + sequence(&mut h, "WVF", false); + assert!(h.state().dialog.is_none(), "WVF must not open a dialog"); + assert!(h.state().message.is_none(), "WVF must not open a message box"); + assert!(h.state().toast.is_none(), "WVF must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVF must not journal"); +} + +#[test] +fn keytip_wvh_() { + let mut h = harness(blank()); + sequence(&mut h, "WVH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.headings"), "WVH must journal view.headings"); +} + +#[test] +fn keytip_wvl_() { + let mut h = harness(blank()); + sequence(&mut h, "WVL", false); + assert!(h.state().dialog.is_none(), "WVL must not open a dialog"); + assert!(h.state().message.is_none(), "WVL must not open a message box"); + assert!(h.state().toast.is_none(), "WVL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVL must not journal"); +} + +#[test] +fn keytip_wvn_() { + let mut h = harness(blank()); + sequence(&mut h, "WVN", false); + assert!(h.state().dialog.is_none(), "WVN must not open a dialog"); + assert!(h.state().message.is_none(), "WVN must not open a message box"); + assert!(h.state().toast.is_none(), "WVN must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVN must not journal"); +} + +#[test] +fn keytip_wvr_() { + let mut h = harness(blank()); + sequence(&mut h, "WVR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.ruler"), "WVR must journal view.ruler"); +} + +#[test] +fn keytip_wvv_() { + let mut h = harness(blank()); + sequence(&mut h, "WVV", false); + assert!(h.state().dialog.is_none(), "WVV must not open a dialog"); + assert!(h.state().message.is_none(), "WVV must not open a message box"); + assert!(h.state().toast.is_none(), "WVV must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVV must not journal"); +} + +#[test] +fn keytip_ww_() { + let mut h = harness(blank()); + sequence(&mut h, "WW", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WW must run view.switchWindows cleanly"); +} + +#[test] +fn keytip_jam_() { + let mut h = harness(blank()); + sequence(&mut h, "JAM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JAM must invoke chart.formatSelection and surface its missing-params error"); +} + +#[test] +fn keytip_jca_() { + let mut h = harness(blank()); + sequence(&mut h, "JCA", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JCA must invoke chart.addElement and surface its missing-params error"); +} + +#[test] +fn keytip_jcc_() { + let mut h = harness(blank()); + sequence(&mut h, "JCC", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JCC must invoke chart.changeType and surface its missing-params error"); +} + +#[test] +fn keytip_jts_() { + let mut h = harness(blank()); + sequence(&mut h, "JTS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JTS must invoke table.style and surface its missing-params error"); +} + +#[test] +fn keytip_jtv_() { + let mut h = harness(blank()); + sequence(&mut h, "JTV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JTV must invoke table.summarizePivot and surface its missing-params error"); +} + +#[test] +fn keytip_fa_() { + let mut h = harness(blank()); + sequence(&mut h, "FA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.saveAs"), "FA must journal file.saveAs"); +} + +#[test] +fn keytip_fc_() { + let mut h = harness(blank()); + sequence(&mut h, "FC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.close"), "FC must journal file.close"); +} + +#[test] +fn keytip_fd_() { + let mut h = harness(blank()); + sequence(&mut h, "FD", false); + assert!(h.state().dialog.is_none(), "FD must not open a dialog"); + assert!(h.state().message.is_none(), "FD must not open a message box"); + assert!(h.state().toast.is_none(), "FD must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "FD must not journal"); +} + +#[test] +fn keytip_fea_() { + let mut h = harness(blank()); + sequence(&mut h, "FEA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.exportPdf"), "FEA must journal file.exportPdf"); +} + +#[test] +fn keytip_fec_() { + let mut h = harness(blank()); + sequence(&mut h, "FEC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.saveAs"), "FEC must journal file.saveAs"); +} + +#[test] +fn keytip_fh_() { + let mut h = harness(blank()); + sequence(&mut h, "FH", false); + assert!(h.state().dialog.is_none(), "FH must not open a dialog"); + assert!(h.state().message.is_none(), "FH must not open a message box"); + assert!(h.state().toast.is_none(), "FH must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "FH must not journal"); +} + +#[test] +fn keytip_fi_() { + let mut h = harness(blank()); + sequence(&mut h, "FI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.properties"), "FI must journal file.properties"); +} + +#[test] +fn keytip_fn_() { + let mut h = harness(blank()); + sequence(&mut h, "FN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.new"), "FN must journal file.new"); +} + +#[test] +fn keytip_fo_() { + let mut h = harness(blank()); + sequence(&mut h, "FO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.open"), "FO must journal file.open"); +} + +#[test] +fn keytip_fp_() { + let mut h = harness(blank()); + sequence(&mut h, "FP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.print"), "FP must journal file.print"); +} + +#[test] +fn keytip_ft_() { + let mut h = harness(blank()); + sequence(&mut h, "FT", false); + assert!(h.state().dialog.is_none(), "FT must not open a dialog"); + assert!(h.state().message.is_none(), "FT must not open a message box"); + assert!(h.state().toast.is_none(), "FT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "FT must not journal"); +} + +#[test] +fn keytip_q_() { + let mut h = harness(blank()); + sequence(&mut h, "Q", false); + assert!(h.state().dialog.is_none(), "Q must not open a dialog"); + assert!(h.state().message.is_none(), "Q must not open a message box"); + assert!(h.state().toast.is_none(), "Q must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "Q must not journal"); +} + +#[test] +fn keytip_6_() { + let mut h = harness(blank()); + sequence(&mut h, "6", false); + assert!(h.state().dialog.is_none(), "6 must not open a dialog"); + assert!(h.state().message.is_none(), "6 must not open a message box"); + assert!(h.state().toast.is_none(), "6 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "6 must not journal"); +} + +#[test] +fn keytip_7_() { + let mut h = harness(blank()); + sequence(&mut h, "7", false); + assert!(h.state().dialog.is_none(), "7 must not open a dialog"); + assert!(h.state().message.is_none(), "7 must not open a message box"); + assert!(h.state().toast.is_none(), "7 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "7 must not journal"); +} + +#[test] +fn keytip_8_() { + let mut h = harness(blank()); + sequence(&mut h, "8", false); + assert!(h.state().dialog.is_none(), "8 must not open a dialog"); + assert!(h.state().message.is_none(), "8 must not open a message box"); + assert!(h.state().toast.is_none(), "8 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "8 must not journal"); +} + +#[test] +fn keytip_9_() { + let mut h = harness(blank()); + sequence(&mut h, "9", false); + assert!(h.state().dialog.is_none(), "9 must not open a dialog"); + assert!(h.state().message.is_none(), "9 must not open a message box"); + assert!(h.state().toast.is_none(), "9 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "9 must not journal"); +} + +#[test] +fn generated_test_count_matches_the_table() { + const EXPECTED: usize = 412; + assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED); +} + +#[test] +fn sample_sequences_work_with_text_before_key() { + // Some IME paths deliver Event::Text ahead of the key press; the matcher + // must pair them either way. One representative of each action kind; + // the dialog opener runs last so it does not eat later input. + // Each step resets first: Escape twice leaves keytip mode so the next + // F10 arms cleanly (a lingering prefix would make F10 disarm instead). + let mut h = harness(blank()); + sequence(&mut h, "H", true); + assert_eq!(h.state().ui.ribbon_tab, "Home"); + reset(&mut h); + sequence(&mut h, "WVG", true); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines")); + reset(&mut h); + sequence(&mut h, "HV", true); + assert_eq!(h.state().keytips.prefix(), Some("HV")); + reset(&mut h); + sequence(&mut h, "NE", true); + assert!(h.state().dialog.is_none() && h.state().message.is_none() && h.state().toast.is_none()); + reset(&mut h); + sequence(&mut h, "HVS", true); + assert!(h.state().dialog.is_some(), "HVS must open the paste-special dialog"); +} + +fn reset(h: &mut Harness) { + key(h, Key::Escape); // clear any prefix, back to the armed root + key(h, Key::Escape); // leave keytip mode entirely +} diff --git a/xtask/src/keytips.rs b/xtask/src/keytips.rs index baa498c..4e3c1dd 100644 --- a/xtask/src/keytips.rs +++ b/xtask/src/keytips.rs @@ -1,13 +1,22 @@ -//! Regenerate the keytip sequence table from `keytips.json`. +//! Regenerate the keytip sequence table and its per-sequence test suite from `keytips.json`. //! -//! `cargo xtask keytips` regenerate crates/ui-egui/src/keytips/generated.rs -//! `cargo xtask keytips --check` fail if the generated file differs from the JSON source +//! `cargo xtask keytips` regenerate the table + `tests/keytips_generated.rs` +//! `cargo xtask keytips --check` fail if either generated file differs from the JSON source +//! +//! Requires the default `engine` feature: the generator probes the live command +//! catalog so each generated test asserts exactly what the command does today +//! (journaled, ran clean without a journal step, or surfaced a missing-params error). use std::fs; use std::path::Path; const TABS: &[&str] = &["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"]; +/// Commands whose empty-params behaviour depends on the host machine (e.g. the +/// system dictionary). These are never probed; their tests assert "invoked +/// something observable" so the checked-in file stays byte-identical everywhere. +const ENV_SENSITIVE: &[&str] = &["review.spelling"]; + /// Format generated source through `rustfmt` so the checked-in file matches /// the emitter byte-for-byte. fn rustfmt(src: &str) -> Result { @@ -29,92 +38,175 @@ fn rustfmt(src: &str) -> Result { Ok(String::from_utf8_lossy(&out.stdout).into_owned()) } -pub fn run(root: &Path, rest: &[&str]) -> Result<(), String> { - let json_path = root.join("crates/ui-egui/src/keytips/keytips.json"); - let out_path = root.join("crates/ui-egui/src/keytips/keytip_table/generated.rs"); - let raw = generate(&fs::read_to_string(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?)?; - // The table is checked into the tree formatted, so emit through rustfmt: - // otherwise `--check` always fails against the fmt'd file. - let generated = rustfmt(&raw)?; - if rest.contains(&"--check") { - let on_disk = fs::read_to_string(&out_path).map_err(|e| format!("read {}: {e}", out_path.display()))?; - if on_disk != generated { - return Err(format!("{} is out of date — run `cargo xtask keytips` and commit the result", out_path.display())); - } - println!("keytips: {} up to date", out_path.display()); - return Ok(()); - } - fs::write(&out_path, &generated).map_err(|e| format!("write {}: {e}", out_path.display()))?; - println!("keytips: wrote {}", out_path.display()); - Ok(()) +#[derive(Clone)] +struct Entry { + seq: String, + kind: String, + target: String, + pending: bool, + excel: String, + note: String, } -pub fn generate(source: &str) -> Result { +/// 0 = ran and journaled, 1 = ran without a journal step, 2 = errored (params +/// missing, disabled, or environment-dependent). +type Probe = std::collections::HashMap; + +#[cfg(feature = "engine")] +fn probe_live_commands(ids: &[String]) -> Probe { + use gridcraft_engine::Session; + let mut map = Probe::new(); + for id in ids { + if ENV_SENSITIVE.contains(&id.as_str()) { + continue; + } + let mut s = Session::new(); + s.new_workbook(); + let status = match s.execute(id, serde_json::json!({})) { + Ok(_) => { + if s.journal.iter().any(|(jid, _)| jid == id) { + 0 + } else { + 1 + } + } + Err(_) => 2, + }; + map.insert(id.clone(), status); + } + map +} + +pub fn run(root: &Path, rest: &[&str]) -> Result<(), String> { + #[cfg(not(feature = "engine"))] + { + let _ = (root, rest); + return Err("cargo xtask keytips needs the default `engine` feature to probe the command catalog".into()); + } + #[cfg(feature = "engine")] + { + let json_path = root.join("crates/ui-egui/src/keytips/keytips.json"); + let table_path = root.join("crates/ui-egui/src/keytips/keytip_table/generated.rs"); + let tests_path = root.join("crates/ui-egui/tests/keytips_generated.rs"); + let source = fs::read_to_string(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?; + let (entries, probe) = parse_and_probe(&source)?; + let generated = + [(table_path.as_path(), rustfmt(&render_table(&entries))?), (tests_path.as_path(), rustfmt(&render_tests(&entries, &probe))?)]; + if rest.contains(&"--check") { + for (path, expected) in &generated { + let on_disk = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; + if on_disk != *expected { + return Err(format!("{} is out of date — run `cargo xtask keytips` and commit the result", path.display())); + } + println!("keytips: {} up to date", path.display()); + } + return Ok(()); + } + for (path, content) in &generated { + fs::write(path, content).map_err(|e| format!("write {}: {e}", path.display()))?; + println!("keytips: wrote {}", path.display()); + } + Ok(()) + } +} + +#[cfg(feature = "engine")] +fn parse_and_probe(source: &str) -> Result<(Vec, Probe), String> { let doc: serde_json::Value = serde_json::from_str(source).map_err(|e| format!("invalid JSON: {e}"))?; let seqs = doc.get("sequences").and_then(|v| v.as_array()).ok_or("missing `sequences` array")?; - let mut rows: Vec<(String, String, String)> = Vec::new(); + let mut entries: Vec = Vec::new(); let mut seen = std::collections::HashSet::new(); - for (i, entry) in seqs.iter().enumerate() { + for (i, e) in seqs.iter().enumerate() { let at = format!("entry {}", i + 1); - let seq = entry.get("seq").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `seq`"))?; + let seq = e.get("seq").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `seq`"))?.to_string(); if seq.is_empty() || seq.len() > 6 || !seq.bytes().all(|b| b.is_ascii_alphanumeric()) { return Err(format!("{at}: seq {seq:?} must be 1-6 ASCII letters/digits (Excel's longest are 5, e.g. sparkline paths)")); } - if !seen.insert(seq.to_string()) { + if !seen.insert(seq.clone()) { return Err(format!("{at}: duplicate seq {seq:?}")); } - let kind = entry.get("kind").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `kind`"))?; - let target = entry.get("target").and_then(|v| v.as_str()).unwrap_or(""); - let pending = entry.get("pending").and_then(|v| v.as_bool()).unwrap_or(false); - let note = entry.get("note").and_then(|v| v.as_str()).unwrap_or(""); - let excel = entry.get("excel").and_then(|v| v.as_str()).unwrap_or(""); + let kind = e.get("kind").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `kind`"))?.to_string(); + let target = e.get("target").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let pending = e.get("pending").and_then(|v| v.as_bool()).unwrap_or(false); if pending && kind != "command" { return Err(format!("{at}: `pending` is only valid with kind `command`")); } - let value = match kind { + match kind.as_str() { "tab" => { - if target.is_empty() || !TABS.contains(&target) { + if target.is_empty() || !TABS.contains(&target.as_str()) { return Err(format!("{at}: tab target {target:?} is not one of {TABS:?}")); } - format!("Entry::Tab({target:?})") } - "command" => { + "command" | "dialog" => { if target.is_empty() { - return Err(format!("{at}: command needs a `target` command id")); + return Err(format!("{at}: {kind} needs a `target`")); } - if pending { format!("Entry::Finish(Action::Pending({target:?}))") } else { format!("Entry::Finish(Action::Command({target:?}))") } } - "dialog" => { - if target.is_empty() { - return Err(format!("{at}: dialog needs a `target` dialog name")); - } - format!("Entry::Finish(Action::Dialog({target:?}))") - } - "continue" => "Entry::Continue".to_string(), + "continue" => {} other => return Err(format!("{at}: unknown kind {other:?}")), - }; - let mut comment = String::new(); - if pending { - comment.push_str(" pending"); } - if !excel.is_empty() { - if !comment.is_empty() { - comment.push(';'); + // A terminal Finish entry on a strict prefix makes the longer sequence + // unreachable (the prefix fires and clears first), e.g. HC (Copy) kills HCP. + if kind == "command" || kind == "dialog" { + for cut in 1..seq.len() { + let prefix = &seq[..cut]; + if entries.iter().any(|e| e.seq == prefix && (e.kind == "command" || e.kind == "dialog")) { + return Err(format!( + "{at}: seq {seq:?} is unreachable — {prefix:?} already fires first; the deeper item needs a menu (item-letter), not a trie sequence" + )); + } } - comment.push_str(&format!(" Excel: {excel}")); } - if !note.is_empty() { - if !comment.is_empty() { - comment.push(';'); - } - comment.push_str(&format!(" {note}")); - } - rows.push((seq.to_string(), value, comment)); + entries.push(Entry { + seq, + kind, + target, + pending, + excel: e.get("excel").and_then(|v| v.as_str()).unwrap_or("").to_string(), + note: e.get("note").and_then(|v| v.as_str()).unwrap_or("").to_string(), + }); } // JSON order is authoritative and curated: the first entry for a tab is its canonical // badge (legacy aliases come later), and groups stay together. Do not sort. + let live_ids: Vec = entries + .iter() + .filter(|e| e.kind == "command" && !e.pending) + .map(|e| e.target.clone()) + .collect::>() + .into_iter() + .collect(); + // Pending targets that already exist fire live at runtime (T6); their tests + // must assert the live behaviour, not the silent one. Nudge the maintainer. + let landed: std::collections::BTreeSet = entries + .iter() + .filter(|e| e.pending && gridcraft_engine::find_command(&e.target).is_some()) + .map(|e| { + eprintln!("keytips: note — pending target {:?} now exists in the catalog; consider dropping `pending`", e.target); + e.target.clone() + }) + .collect(); + let mut probe = probe_live_commands(&live_ids); + for id in landed { + let mut s = gridcraft_engine::Session::new(); + s.new_workbook(); + let status = match s.execute(&id, serde_json::json!({})) { + Ok(_) => { + if s.journal.iter().any(|(jid, _)| jid == &id) { + 0 + } else { + 1 + } + } + Err(_) => 2, + }; + probe.insert(id, status); + } + Ok((entries, probe)) +} + +fn render_table(entries: &[Entry]) -> String { let mut out = String::from( "//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file.\n\n\ use super::{Action, Entry};\n\n\ @@ -123,13 +215,191 @@ pub fn generate(source: &str) -> Result { /// listed here. See keytip_table.rs for the helpers that read this table.\n\ pub const SEQUENCES: &[(&str, Entry)] = &[\n", ); - for (seq, value, comment) in &rows { - out.push_str(&format!(" ({seq:?}, {value}),")); + for e in entries { + let value = match e.kind.as_str() { + "tab" => format!("Entry::Tab({:?})", e.target), + "command" if e.pending => format!("Entry::Finish(Action::Pending({:?}))", e.target), + "command" => format!("Entry::Finish(Action::Command({:?}))", e.target), + "dialog" => format!("Entry::Finish(Action::Dialog({:?}))", e.target), + "continue" => "Entry::Continue".to_string(), + // All kinds are validated during parse; anything else cannot occur. + _ => String::new(), + }; + let mut comment = String::new(); + if e.pending { + comment.push_str(" pending"); + } + if !e.excel.is_empty() { + if !comment.is_empty() { + comment.push(';'); + } + comment.push_str(&format!(" Excel: {}", e.excel)); + } + if !e.note.is_empty() { + if !comment.is_empty() { + comment.push(';'); + } + comment.push_str(&format!(" {}", e.note)); + } + out.push_str(&format!(" ({:?}, {value}),", e.seq)); if !comment.is_empty() { out.push_str(&format!(" //{comment}")); } out.push('\n'); } out.push_str("];\n"); - Ok(out) + out } + +fn test_name(seq: &str) -> String { + format!("keytip_{}_", seq.to_ascii_lowercase()) +} + +fn render_tests(entries: &[Entry], probe: &Probe) -> String { + let mut out = String::from(HEADER); + for e in entries { + let body = match e.kind.as_str() { + "tab" => format!( + "sequence(&mut h, {seq:?}, false);\n assert_eq!(h.state().ui.ribbon_tab, {tab:?}, \"{seq} must switch to the {tab} tab\");", + seq = e.seq, + tab = e.target, + ), + "command" if e.pending && !probe.contains_key(&e.target) => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().dialog.is_none(), \"{seq} must not open a dialog\");\n assert!(h.state().message.is_none(), \"{seq} must not open a message box\");\n assert!(h.state().toast.is_none(), \"{seq} must not raise a toast\");\n assert!(h.state().session.journal.is_empty(), \"{seq} must not journal\");", + seq = e.seq, + ), + "command" => match probe.get(&e.target).copied().unwrap_or(2) { + 0 => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().session.journal.iter().any(|(id, _)| id == {id:?}), \"{seq} must journal {id}\");", + seq = e.seq, + id = e.target, + ), + 1 => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().message.is_none() && h.state().toast.is_none(), \"{seq} must run {id} cleanly\");", + seq = e.seq, + id = e.target, + ), + _ => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().message.is_some() || h.state().toast.is_some(), \"{seq} must invoke {id} and surface its missing-params error\");", + seq = e.seq, + id = e.target, + ), + }, + "dialog" => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().dialog.is_some(), \"{seq} must open the {dialog} dialog\");", + seq = e.seq, + dialog = e.target, + ), + "continue" => format!( + "sequence(&mut h, {seq:?}, false);\n assert_eq!(h.state().keytips.prefix(), Some({seq:?}), \"{seq} must stay armed waiting for more input\");", + seq = e.seq, + ), + // All kinds are validated during parse; anything else cannot occur. + _ => String::new(), + }; + out.push_str(&format!("\n#[test]\nfn {}() {{\n let mut h = harness(blank());\n {body}\n}}\n", test_name(&e.seq))); + } + out.push_str(&format!( + "\n#[test]\nfn generated_test_count_matches_the_table() {{\n const EXPECTED: usize = {};\n assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED);\n}}\n", + entries.len() + )); + out.push_str(SAMPLE_TEXT_FIRST); + out +} + +const HEADER: &str = r#"//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file. +//! +//! One end-to-end test per keytip sequence, through the real egui input path. +//! Assertions are generated from a probe of the live engine catalog (blank +//! workbook, empty params), so each test proves exactly what its command does +//! today: journaled, ran clean, surfaced a missing-params error, opened a +//! dialog, switched tabs, waited for more input, or stayed silent (pending). + +use egui::{Event, Key, Modifiers}; +use egui::os::OperatingSystem; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness(session: Session) -> Harness { + let mut h = egui_kittest::Harness::builder() + .with_os(OperatingSystem::Nix) + .with_size(egui::vec2(1200.0, 800.0)) + .build_ui_state( + |ui, app: &mut SheetApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +fn blank() -> Session { + let mut s = Session::new(); + s.new_workbook(); + s +} + +fn key_event(key: Key, pressed: bool, modifiers: Modifiers) -> Event { + Event::Key { key, physical_key: None, pressed, repeat: false, modifiers } +} + +fn events(h: &mut Harness, events: impl IntoIterator) { + h.input_mut().events.extend(events); + h.run_steps(2); +} + +fn key(h: &mut Harness, key: Key) { + events(h, [key_event(key, true, Modifiers::NONE), key_event(key, false, Modifiers::NONE)]); +} + +fn letter(h: &mut Harness, key: Key, text_first: bool) { + let text = Event::Text(key.name().to_ascii_lowercase()); + let pressed = key_event(key, true, Modifiers::NONE); + let pair = if text_first { [text, pressed] } else { [pressed, text] }; + events(h, pair.into_iter().chain([key_event(key, false, Modifiers::NONE)])); +} + +fn sequence(h: &mut Harness, seq: &str, text_first: bool) { + key(h, Key::F10); + for ch in seq.chars() { + let k = Key::from_name(&ch.to_string()).expect("sequence letters map to keys"); + letter(h, k, text_first); + } +} + +"#; + +const SAMPLE_TEXT_FIRST: &str = r#" +#[test] +fn sample_sequences_work_with_text_before_key() { + // Some IME paths deliver Event::Text ahead of the key press; the matcher + // must pair them either way. One representative of each action kind; + // the dialog opener runs last so it does not eat later input. + // Each step resets first: Escape twice leaves keytip mode so the next + // F10 arms cleanly (a lingering prefix would make F10 disarm instead). + let mut h = harness(blank()); + sequence(&mut h, "H", true); + assert_eq!(h.state().ui.ribbon_tab, "Home"); + reset(&mut h); + sequence(&mut h, "WVG", true); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines")); + reset(&mut h); + sequence(&mut h, "HV", true); + assert_eq!(h.state().keytips.prefix(), Some("HV")); + reset(&mut h); + sequence(&mut h, "NE", true); + assert!(h.state().dialog.is_none() && h.state().message.is_none() && h.state().toast.is_none()); + reset(&mut h); + sequence(&mut h, "HVS", true); + assert!(h.state().dialog.is_some(), "HVS must open the paste-special dialog"); +} + +fn reset(h: &mut Harness) { + key(h, Key::Escape); // clear any prefix, back to the armed root + key(h, Key::Escape); // leave keytip mode entirely +} +"#;